Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81cc78ce90 | |||
| e8a7abffe3 | |||
| f7ad6a4b0d | |||
| 1bfb50de8a | |||
| 3bc7c96191 | |||
| 032bf1acf3 | |||
| 8bc765c1a3 | |||
| da061240af | |||
| 407a1b967c | |||
| ad756cd6bd | |||
| 6e11bc18d6 | |||
| c8eee9f727 | |||
| 01a53ce1c0 | |||
| 0eddf31f92 | |||
| e257ba717b | |||
| 7a797165fd | |||
| a659413090 | |||
| 08e7ec71d9 | |||
| c97f14980b | |||
| 5d090f496b | |||
| d6b46037b4 | |||
| 5b5acfdd25 | |||
| 926338ba62 | |||
| cc420013f9 | |||
| ba0a21feca | |||
| 70b9d20c64 | |||
| dee2b85daa | |||
| 119e9044a8 | |||
| e6a260bd6a | |||
| a23ede433b | |||
| 346b0a139d | |||
| f5cda2901f | |||
| 2dda5c8706 | |||
| d6aa2ddce3 | |||
| 6f9801a185 | |||
| c82edae338 | |||
| 57327c1f73 | |||
| e3a22bf7d0 | |||
| 3718d914fd | |||
| 97a8b63c9c | |||
| d6d0b66078 | |||
| b173598320 | |||
| f211eede3b | |||
| b123ed9fa3 | |||
| 2b0d8e5423 | |||
| 49b8024fbe | |||
| be987e1434 | |||
| 27e2b89fcf | |||
| c261a746a3 | |||
| c290e60af1 | |||
| 19142a30c6 | |||
| 2ca317d742 | |||
| 45963aeea5 | |||
| 4929f2cc51 | |||
| 90e51f5f09 | |||
| 9b7aeccd48 | |||
| 10d55bd25c | |||
| 16224cd5a6 | |||
| d62d4c852a | |||
| 6d6349c79d | |||
| 9d2eec1732 | |||
| d6da1656b8 | |||
| ae7bac65d5 | |||
| 99c4d6dff8 | |||
| 74600b4b25 | |||
| 6cae2ed3ac | |||
| 611b56186d | |||
| dfa673e6e9 | |||
| be74e3358a | |||
| 4cc85d595f | |||
| b2d7a12033 | |||
| 65cffbba0c | |||
| 5f8e3b9760 | |||
| 397a15349c | |||
| 235a4af0a9 | |||
| b171d48f87 | |||
| bfeba94e20 | |||
| aa0bde0999 | |||
| 72e4a6b94a | |||
| 88f00f5638 | |||
| db21800d9f | |||
| 4a63f469bf | |||
| 810852997a | |||
| 02d53beeac | |||
| d589d8bac2 | |||
| 0deb1d11fc | |||
| 6cc6153abb | |||
| af163dac62 | |||
| 8c4be03ab7 | |||
| fce57f8ee2 | |||
| d48423da77 | |||
| f691578698 | |||
| 42a4a46dd9 | |||
| e1d715bc38 | |||
| 4285290452 | |||
| d79834eb50 | |||
| 1244706b6e | |||
| a1a7699516 | |||
| 577741907f | |||
| ba35935d2e | |||
| a7df8d837f | |||
| 820c577f5f | |||
| 8b3a1a40f2 | |||
| 21238c8e41 | |||
| 73f145a35c | |||
| 3184305c41 | |||
| 06dc40fd3e | |||
| c7ac333cb7 | |||
| 1d6e415422 | |||
| c89c5d1cc3 | |||
| 19dc966abd | |||
| 077c1f439c | |||
| 9140138843 | |||
| 618e3a32f2 | |||
| 681f8670f5 | |||
| 0a44124c20 | |||
| eb64a9ad43 | |||
| 5a5ff40efc | |||
| cd6726d451 | |||
| cbc973a559 | |||
| d968983ceb | |||
| 5175d50aa8 | |||
| e25818b451 | |||
| 4453e66bcf | |||
| 19432f33ba | |||
| 5c7170268e | |||
| 79d2c023e6 | |||
| 864175fae6 | |||
| 7fc2412c9c | |||
| 3fdbe9a4f7 | |||
| 399b5d65e5 | |||
| 24e5a3fac4 | |||
| 31420b6777 | |||
| 7379484932 | |||
| d36ce10ab3 | |||
| 57807f09c8 | |||
| 0745d75c2f | |||
| a78f47ecfe | |||
| 1df705c4f9 | |||
| f7ed2c5273 | |||
| 8bd4c936f8 | |||
| 34e55d9cb5 | |||
| 8a8596a66b | |||
| 40ab4ceb2b | |||
| a8dbbba6e3 | |||
| 5b0cc0130e | |||
| bb32a0a8ba | |||
| e23894c2e6 | |||
| e0efa80f44 | |||
| cb734f8df1 | |||
| 3197d415e0 | |||
| 9dfc42c723 | |||
| 14b973ec5f | |||
| 81a3cea9c7 | |||
| dc6138076f | |||
| a9f7285c0f | |||
| 7106a461b5 | |||
| 69e3ebcc26 | |||
| f80f0f5f02 | |||
| a447bc8104 | |||
| 32f54f157b | |||
| 9c9f1a3000 | |||
| 9d7b13d4c9 | |||
| 60cbf59ddf | |||
| 6f00e630e0 | |||
| 7599f71c17 | |||
| 411cb0efa7 | |||
| 00ec554783 | |||
| d25d3a460a | |||
| 8eec90c35c | |||
| a596e01ef2 | |||
| 43ebc37f87 | |||
| 6223f10fae | |||
| c16e927288 | |||
| 6ef33204e8 | |||
| 3f9d62248a | |||
| 18fbd8e56c | |||
| e5c74d05aa | |||
| 82c22dc18e | |||
| 66a74b5693 | |||
| 367b8bdb5c | |||
| b865c88d99 | |||
| 53b39010a0 | |||
| 7ad8bed7df | |||
| 01ea4c422a | |||
| 7eb090a622 | |||
| 48956dc016 | |||
| 2eb8a0dc1d | |||
| c53ffebece | |||
| 06d0aa53bc | |||
| 28b1424357 | |||
| e852439b6a | |||
| 5850c514f2 | |||
| 11a49fd687 | |||
| be9ba1366e | |||
| dbe67f1034 | |||
| 7eecfa3275 | |||
| e1b4039a52 | |||
| 9f2cfb5a9f | |||
| 41e66825d0 | |||
| a78393ca91 | |||
| 8dde32cb21 | |||
| 1538513fb7 | |||
| 30c8aef24e | |||
| 8daa65d93e | |||
| df5fcae69b | |||
| e3b63908de | |||
| 8c53de9cc7 | |||
| 78cb758a26 | |||
| 8fea9ef212 | |||
| 38fdc24a49 | |||
| 069c64ab08 | |||
| 2005fb1421 | |||
| 04b15fee2f | |||
| eda3dda613 | |||
| 03be98b571 | |||
| 93bf103f03 | |||
| c8f485cd93 | |||
| 3a301c517d | |||
| d17c577ccd | |||
| 8c331c25eb | |||
| 8341ef5731 | |||
| 40a29d927f | |||
| 9846c2243c | |||
| c1dc4fe589 | |||
| 06052b729b | |||
| d7c912b53e | |||
| b792ebbd45 | |||
| e7071047e3 | |||
| 6496288c4e | |||
| 1ab8987f2d | |||
| 034b39bec5 | |||
| 5368ae3d37 | |||
| f963742009 | |||
| a8ae9dbc6a | |||
| b55a031971 | |||
| 27ffedbdee | |||
| 493f08afaf | |||
| 0659da24fa | |||
| 5596dad111 | |||
| a5b7561c51 | |||
| fe23e4bad9 | |||
| 5b3b8265e5 | |||
| a7cbf866e8 | |||
| 7fae21d7ac | |||
| 55bf63210a | |||
| 2b72c24dbe | |||
| 455365a71d | |||
| 1f6b575440 | |||
| 86304018dd | |||
| c44ab9602c | |||
| 13be114823 | |||
| f793eb06be | |||
| 61532f7b45 | |||
| a69a74bf37 | |||
| 6d4d5468f9 | |||
| 7015d44da7 | |||
| 172867527b | |||
| 323e8cf1f0 | |||
| 172d10d7cb | |||
| 945d456f9c | |||
| a4f635265b | |||
| f45f951af8 | |||
| 883852a828 | |||
| 90052257bb | |||
| 1184d3d77a | |||
| 12280bb35b | |||
| 17f3f2dc63 | |||
| 3d6ca17a9a | |||
| 01863a70a8 | |||
| 801cc1bc74 | |||
| 9ed8d835c0 | |||
| b754891d45 | |||
| 9cc6d48493 | |||
| b865538d6e | |||
| abacaddf8a | |||
| b86c5daa2c | |||
| df8d7d9ff0 | |||
| 8c7d3d4893 | |||
| b88f041bd8 | |||
| e860b96bd8 | |||
| b2ef544513 | |||
| ca00ee1a42 | |||
| 511d8461c2 | |||
| fe42efadfa | |||
| c1e73320b1 | |||
| 47dd6d61b0 | |||
| 212557610b | |||
| 5e6a7731f4 | |||
| 65c63dfc5d | |||
| ae4e7cb01f | |||
| a233c72dad | |||
| b267b83248 | |||
| 6c2ae4dc5a | |||
| 0a884e85df | |||
| 47098f60dd |
@@ -11,7 +11,10 @@ rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
rustflags = ["-C", "target-cpu=x86-64-v3"]
|
||||
|
||||
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
|
||||
rustflags = ["-C", "target-cpu=apple-m1"]
|
||||
rustflags = [
|
||||
"-C", "target-cpu=apple-m1",
|
||||
"-C", "link-arg=-headerpad_max_install_names"
|
||||
]
|
||||
|
||||
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
|
||||
rustflags = ["-C", "target-cpu=cortex-a76"]
|
||||
@@ -21,3 +24,4 @@ CC = "clang"
|
||||
CXX = "clang++"
|
||||
AR = "llvm-ar"
|
||||
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||
CMAKE_POLICY_VERSION_MINIMUM = "3.5" # for freetype
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Bugs only! No feature requests
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: Bug
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device (please complete the following information):**
|
||||
- OS: [e.g. Windows 11]
|
||||
- CPU: [e.g. Intel i7-1365U]
|
||||
- GPU: [e.g. Nvidia 5060 Ti]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -4,15 +4,19 @@ permissions:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
|
||||
NETPLAY_ID: ${{ github.ref_type == 'tag' && secrets.NETPLAY_ID || 'dev' }}
|
||||
BUILD_PROFILE: ${{ github.ref_type == 'tag' && 'release' || 'dev' }}
|
||||
BUILD_DIR: ${{ github.ref_type == 'tag' && 'release' || 'debug' }}
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -30,7 +34,7 @@ jobs:
|
||||
arch: linux-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -40,7 +44,8 @@ jobs:
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
run:
|
||||
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
@@ -53,23 +58,21 @@ jobs:
|
||||
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
|
||||
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
|
||||
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
|
||||
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
|
||||
libdecor-0-dev liburing-dev libfontconfig-dev
|
||||
version: ${{ matrix.target }}-1.0
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
force-url:
|
||||
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{
|
||||
matrix.llvm }}.tar.xz
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --no-default-features -v --target=${{ matrix.target }}
|
||||
cargo build --release -v --target=${{ matrix.target }}
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
env:
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -92,7 +95,7 @@ jobs:
|
||||
arch: windows-aarch64
|
||||
llvm: woa64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -109,19 +112,18 @@ jobs:
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
force-url:
|
||||
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm
|
||||
}}.exe
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --no-default-features -v --target=${{ matrix.target }}
|
||||
cargo build --release -v --target=${{ matrix.target }}
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
|
||||
env:
|
||||
CC: clang-cl
|
||||
CXX: clang-cl
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
- name: Upload unsigned artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -161,7 +163,7 @@ jobs:
|
||||
arch: macos-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -171,48 +173,129 @@ jobs:
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
run:
|
||||
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
force-url:
|
||||
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-macOS-${{
|
||||
matrix.llvm }}.tar.xz
|
||||
- name: Build
|
||||
run: |
|
||||
cargo install cargo-bundle
|
||||
brew install molten-vk
|
||||
cargo build --no-default-features -v --target=${{ matrix.target }}
|
||||
cargo bundle --release --target=${{ matrix.target }} --format osx
|
||||
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
|
||||
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64
|
||||
install_name_tool -change /opt/homebrew/opt/libpng/lib/libpng16.16.dylib \
|
||||
@executable_path/../Frameworks/libpng16.16.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --no-default-features -v --target=${{ matrix.target }}
|
||||
mv target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli
|
||||
cargo bundle --profile ${{ env.BUILD_PROFILE }} --target=${{ matrix.target }} --format osx
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
env:
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
MACOSX_DEPLOYMENT_TARGET: "15.7"
|
||||
MACOSX_DEPLOYMENT_TARGET: "15.0"
|
||||
- name: Sign Bundle Dev
|
||||
if: ${{ github.ref_type != 'tag' }}
|
||||
run: |
|
||||
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
- name: Import Codesign Certs
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
uses: apple-actions/import-codesign-certs@v7
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_FILE_BASE64 }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATES_PASSWORD }}
|
||||
- name: Sign Bundle Release
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: |
|
||||
cp data/macos/embedded.provisionprofile target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/embedded.provisionprofile
|
||||
codesign -f --entitlements data/macos/entitlements_prod.plist --deep --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/Frameworks/libMoltenVK.dylib
|
||||
codesign -f --entitlements data/macos/entitlements_child.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
codesign -f --entitlements data/macos/entitlements_prod.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
xcrun notarytool submit --wait --apple-id "${{ secrets.APPLE_ID }}" --team-id "${{ secrets.APPLE_DEVELOPER_ID }}" --password "${{ secrets.APPLE_APP_PASSWORD }}" gopher64-${{ matrix.arch }}.zip
|
||||
xcrun stapler staple target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
xcrun stapler staple target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: target/${{ matrix.target }}/release/bundle/osx
|
||||
path: target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx
|
||||
|
||||
build-android:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
platform: 33
|
||||
ndk: 29.0.14206865
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: x86_64-linux-android,aarch64-linux-android
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
packages: "platforms;android-${{ matrix.platform }} ndk;${{ matrix.ndk }}"
|
||||
- name: Decode keystore and create jks
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
working-directory: android-project
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > app/keystore.jks
|
||||
|
||||
cat <<EOF > keystore.properties
|
||||
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
|
||||
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
keyAlias=gopher64
|
||||
storeFile=keystore.jks
|
||||
EOF
|
||||
- name: Build
|
||||
working-directory: android-project
|
||||
run: |
|
||||
cargo install cargo-ndk
|
||||
./gradlew assemble${{ env.BUILD_PROFILE == 'release' && 'Release' || 'Debug' }}
|
||||
mkdir output
|
||||
cp app/build/outputs/apk/${{ env.BUILD_DIR }}/app-${{ env.BUILD_DIR }}.apk output/gopher64-android.apk
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-android
|
||||
path: android-project/output/gopher64-android.apk
|
||||
archive: false
|
||||
|
||||
collect-artifacts:
|
||||
name: Collect release artifacts
|
||||
needs: [build-linux, build-windows, build-macos, build-android]
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all workflow artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
skip-decompress: true
|
||||
merge-multiple: true
|
||||
path: release-artifacts
|
||||
- name: Add files to release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
files: release-artifacts/*
|
||||
- name: Setup butler
|
||||
uses: remarkablegames/setup-butler@v3
|
||||
- name: Upload to itch.io
|
||||
run: |
|
||||
butler push release-artifacts/gopher64-linux-x86_64 loganmc10/gopher64:linux-x86_64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-linux-aarch64 loganmc10/gopher64:linux-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-windows-x86_64.exe loganmc10/gopher64:windows-x86_64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-windows-aarch64.exe loganmc10/gopher64:windows-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-macos-aarch64.zip loganmc10/gopher64:macos-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-android.apk loganmc10/gopher64:android --userversion ${GITHUB_REF_NAME#v}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
|
||||
|
||||
@@ -2,9 +2,10 @@ name: Rust-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -16,14 +17,17 @@ jobs:
|
||||
lint-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ""
|
||||
components: clippy, rustfmt
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
run:
|
||||
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
@@ -36,17 +40,23 @@ jobs:
|
||||
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
|
||||
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
|
||||
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
|
||||
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
|
||||
libdecor-0-dev liburing-dev libfontconfig-dev gettext
|
||||
version: lint-1.0
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
force-url:
|
||||
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
|
||||
- name: Rustfmt Check
|
||||
uses: actions-rust-lang/rustfmt@v1
|
||||
- name: Lint
|
||||
run: |
|
||||
cargo install slint-tr-extractor
|
||||
cargo clippy -- -Dwarnings
|
||||
cargo clippy --no-default-features -- -Dwarnings
|
||||
find -name \*.slint | sort | xargs slint-tr-extractor -o /tmp/translations.pot
|
||||
msgcmp --use-untranslated /tmp/translations.pot data/translations/gopher64.pot
|
||||
msgcmp --use-untranslated data/translations/gopher64.pot /tmp/translations.pot
|
||||
find data/translations -name "*.po" | xargs -I {} msgcmp {} data/translations/gopher64.pot
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
checks: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: rustsec/audit-check@v2
|
||||
|
||||
@@ -1,50 +1,65 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.17"
|
||||
version = "1.1.34"
|
||||
edition = "2024"
|
||||
rust-version = "1.95.0"
|
||||
rust-version = "1.97.0"
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
|
||||
gui = ["dep:rfd", "dep:slint", "dep:open", "dep:slint-build"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0"
|
||||
zip = "8.0"
|
||||
sevenz-rust2 = "0.20"
|
||||
sevenz-rust2 = "0.21"
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.5"
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-skia", "accessibility"], optional = true }
|
||||
open = "5.3"
|
||||
sha2 = "0.11"
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source-static", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
rfd = {version = "0.17", optional = true }
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "fs", "process"] }
|
||||
spin_sleep = "1.3"
|
||||
encoding_rs = "0.8"
|
||||
mimalloc = "0.1"
|
||||
regex = "1.11"
|
||||
tokio-tungstenite = "0.29"
|
||||
tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] }
|
||||
matchbox_socket = "0.14"
|
||||
ggrs = { version = "0.13", features = ["sync-send"] }
|
||||
rustc-hash = "2.1"
|
||||
futures = "0.3"
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
rand = "0.10"
|
||||
rand = { version = "0.10", features = ["serde"] }
|
||||
fatfs = "0.3"
|
||||
semver = "1.0"
|
||||
postcard = { version = "1.1", features = ["use-std"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.13", features = ["json", "query"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] }
|
||||
discord-rich-presence = "1.1"
|
||||
quick-xml = "0.41"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-femtovg-wgpu", "accessibility"], optional = true }
|
||||
rfd = {version = "0.17", optional = true }
|
||||
dirs = "6.0"
|
||||
open = { version = "5.3", optional = true }
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
|
||||
jni = "0.22"
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1"
|
||||
bindgen = "0.72"
|
||||
sdl3-src = "3.4"
|
||||
slint-build = { version = "1.12", optional = true }
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
@@ -52,12 +67,11 @@ strip = true
|
||||
inherits = "release"
|
||||
debug = true
|
||||
strip = false
|
||||
panic = "unwind"
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "Gopher64"
|
||||
identifier = "io.github.gopher64.gopher64"
|
||||
category = "public.app-category.games"
|
||||
icon = ["data/icon/gopher64_128x128.png","data/icon/gopher64_256x256.png","data/icon/gopher64_512x512.png"]
|
||||
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib","/opt/homebrew/opt/freetype/lib/libfreetype.6.dylib","/opt/homebrew/opt/libpng/lib/libpng16.16.dylib"]
|
||||
osx_minimum_system_version = "15.7"
|
||||
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib"]
|
||||
osx_minimum_system_version = "15.0"
|
||||
|
||||
@@ -11,11 +11,21 @@ Gopher64 is a cross-platform N64 emulator. Some notable features:
|
||||
|
||||
## download
|
||||
|
||||
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
|
||||
<a href="https://loganmc10.itch.io/gopher64"><img src="https://static.itch.io/images/badge.svg" width="200" ></a>
|
||||
|
||||
MacOS: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip
|
||||
Windows:
|
||||
* Standalone executable: [gopher64-windows-x86_64.exe](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe)
|
||||
|
||||
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
|
||||
MacOS:
|
||||
* App Bundle: [gopher64-macos-aarch64.zip](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip)
|
||||
* Homebrew: `brew install --cask gopher64`
|
||||
|
||||
Linux:
|
||||
* Standalone executable: [gopher64-linux-x86_64](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64)
|
||||
* Flatpak: `flatpak install flathub io.github.gopher64.gopher64`
|
||||
|
||||
Android:
|
||||
* APK: [gopher64-android.apk](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-android.apk)
|
||||
|
||||
## wiki
|
||||
|
||||
@@ -31,7 +41,9 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
|
||||
|
||||
## netplay
|
||||
|
||||
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
Gopher64 supports P2P netplay (online play with others).
|
||||
|
||||
Please read the [guide](https://github.com/gopher64/gopher64/wiki/Netplay-Guide) before trying out netplay.
|
||||
|
||||
## portable mode
|
||||
|
||||
@@ -56,7 +68,9 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
|
||||
|
||||
## contributing
|
||||
|
||||
I am very open to contributions! Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
|
||||
PRs that are vibe coded and/or co-authored by AI agents (Plan/Agent mode) are not permitted. If you are going to submit a PR, it should be scoped to a single feature/improvement.
|
||||
|
||||
Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
|
||||
|
||||
## license
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
jniLibs/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
keystore.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.aab
|
||||
*.apk
|
||||
output-metadata.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
app/src/main/java/org/libsdl/app/
|
||||
@@ -0,0 +1,4 @@
|
||||
set(ANDROID_PLATFORM $ENV{ANDROID_PLATFORM})
|
||||
set(ANDROID_ABI $ENV{ANDROID_ABI})
|
||||
set(ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES ON)
|
||||
include("$ENV{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake")
|
||||
@@ -0,0 +1,161 @@
|
||||
import groovy.json.JsonSlurper
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.gopher64.gopher64"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.gopher64.gopher64"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = semverToVersionCode(cargoPackageVersion())
|
||||
versionName = cargoPackageVersion()
|
||||
ndk {
|
||||
abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes.add("lib/**/libsevenz_rust2*.so")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun cargoPackageVersion(packageName: String = "gopher64"): String {
|
||||
val repoRoot = rootDir.parentFile
|
||||
val jsonText = providers.exec {
|
||||
workingDir(repoRoot)
|
||||
commandLine("cargo", "metadata", "--format-version", "1", "--no-deps")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
val parsedJson = JsonSlurper().parseText(jsonText) as Map<String, Any>
|
||||
val packages = parsedJson["packages"] as List<Map<String, Any>>
|
||||
return packages.first { it["name"] == packageName }["version"] as String
|
||||
}
|
||||
|
||||
fun semverToVersionCode(version: String): Int {
|
||||
val parts = version.substringBefore('-').split(".")
|
||||
val (major, minor, patch) = Triple(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
|
||||
|
||||
return major * 1_000_000 + minor * 1_000 + patch
|
||||
}
|
||||
|
||||
val ndkBuild = tasks.register<Exec>("ndkBuild") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
workingDir = rootDir.parentFile
|
||||
val toolchainPath = "$rootDir/android.toolchain.cmake"
|
||||
environment("CMAKE_TOOLCHAIN_FILE", toolchainPath)
|
||||
|
||||
var minSdk = android.defaultConfig.minSdk
|
||||
var ndkDir = androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath
|
||||
environment("ANDROID_NDK_HOME", "$ndkDir")
|
||||
environment("ANDROID_NDK_ROOT", "$ndkDir")
|
||||
environment("LIBCLANG_PATH", "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/musl/lib")
|
||||
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs"
|
||||
|
||||
commandLine(
|
||||
"cargo", "ndk",
|
||||
"--link-libcxx-shared",
|
||||
"-P", "$minSdk",
|
||||
"-t", "arm64-v8a",
|
||||
"-t", "x86_64",
|
||||
"-o", jniLibsFolder,
|
||||
"build", "--lib",
|
||||
"--profile", if (isRelease) "release" else "dev",
|
||||
)
|
||||
}
|
||||
|
||||
val sdlLibsArm64 = tasks.register<Copy>("sdlLibsArm64") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/arm64-v8a"
|
||||
|
||||
from("$rootDir/../target/aarch64-linux-android/$jniType")
|
||||
into(jniLibsFolder)
|
||||
include("libSDL*")
|
||||
}
|
||||
|
||||
val sdlLibsX64 = tasks.register<Copy>("sdlLibsX64") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/x86_64"
|
||||
|
||||
from("$rootDir/../target/x86_64-linux-android/$jniType")
|
||||
into(jniLibsFolder)
|
||||
include("libSDL*")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(sdlLibsArm64)
|
||||
dependsOn(sdlLibsX64)
|
||||
}
|
||||
|
||||
tasks.named("sdlLibsArm64") {
|
||||
dependsOn(ndkBuild)
|
||||
}
|
||||
|
||||
tasks.named("sdlLibsX64") {
|
||||
dependsOn(ndkBuild)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {
|
||||
java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it
|
||||
java.lang.String clipboardGetText();
|
||||
boolean clipboardHasText();
|
||||
void clipboardSetText(java.lang.String);
|
||||
int createCustomCursor(int[], int, int, int, int);
|
||||
void destroyCustomCursor(int);
|
||||
android.app.Activity getContext();
|
||||
boolean getManifestEnvironmentVariables();
|
||||
android.view.Surface getNativeSurface();
|
||||
void initTouch();
|
||||
boolean isAndroidTV();
|
||||
boolean isChromebook();
|
||||
boolean isDeXMode();
|
||||
boolean isTablet();
|
||||
void manualBackButton();
|
||||
int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]);
|
||||
void minimizeWindow();
|
||||
boolean openURL(java.lang.String);
|
||||
void requestPermission(java.lang.String, int);
|
||||
boolean showToast(java.lang.String, int, int, int, int);
|
||||
boolean sendMessage(int, int);
|
||||
boolean setActivityTitle(java.lang.String);
|
||||
boolean setCustomCursor(int);
|
||||
void setOrientation(int, int, boolean, java.lang.String);
|
||||
boolean setRelativeMouseEnabled(boolean);
|
||||
boolean setSystemCursor(int);
|
||||
void setWindowStyle(boolean);
|
||||
boolean shouldMinimizeOnFocusLoss();
|
||||
boolean showTextInput(int, int, int, int, int);
|
||||
boolean supportsRelativeMouse();
|
||||
int openFileDescriptor(java.lang.String, java.lang.String);
|
||||
boolean showFileDialog(java.lang.String[], boolean, boolean, int);
|
||||
java.lang.String getPreferredLocales();
|
||||
java.lang.String formatLocale(java.util.Locale);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager {
|
||||
void closeDevice(int);
|
||||
boolean initialize(boolean, boolean);
|
||||
boolean openDevice(int);
|
||||
boolean readReport(int, byte[], boolean);
|
||||
int writeReport(int, byte[], boolean);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager {
|
||||
void registerAudioDeviceCallback();
|
||||
void unregisterAudioDeviceCallback();
|
||||
void audioSetThreadPriority(boolean, int);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager {
|
||||
void joystickSetSensorsEnabled(int, boolean);
|
||||
void pollInputDevices();
|
||||
void joystickSetLED(int, int, int, int);
|
||||
void pollHapticDevices();
|
||||
void hapticRun(int, float, int);
|
||||
void hapticRumble(int, float, float, int);
|
||||
void hapticStop(int);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:required="true"
|
||||
android:version="0x00401000" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Gopher64"
|
||||
android:appCategory="game"
|
||||
android:hardwareAccelerated="true">
|
||||
<activity android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:name=".SlintActivity" android:exported="true">
|
||||
<meta-data android:name="android.app.lib_name" android:value="gopher64" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".N64Activity"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.github.gopher64.gopher64
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import org.libsdl.app.SDLActivity
|
||||
|
||||
class N64Activity : SDLActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val powerManager = getContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
if (powerManager.isSustainedPerformanceModeSupported) {
|
||||
Log.v("SDL", "Enabling sustained performance mode")
|
||||
window.setSustainedPerformanceMode(true)
|
||||
} else {
|
||||
Log.v("SDL", "Sustained performance mode not supported")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLibraries(): Array<String> = arrayOf(
|
||||
"SDL3",
|
||||
"SDL3_ttf",
|
||||
"gopher64"
|
||||
)
|
||||
|
||||
override fun getMainFunction(): String = "gopher64_sdl_main"
|
||||
|
||||
override fun getArguments(): Array<String> {
|
||||
val intent = intent ?: return super.getArguments()
|
||||
val args = intent.getStringArrayExtra("args") ?: return super.getArguments()
|
||||
|
||||
val dataIntent = Intent()
|
||||
val file_path = intent.getStringExtra("file_path")
|
||||
if (file_path != null) {
|
||||
dataIntent.putExtra("file_path", file_path)
|
||||
}
|
||||
val cheats_path = intent.getStringExtra("cheats_path")
|
||||
if (cheats_path != null) {
|
||||
dataIntent.putExtra("cheats_path", cheats_path)
|
||||
}
|
||||
setResult(RESULT_OK, dataIntent)
|
||||
return args
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.github.gopher64.gopher64
|
||||
|
||||
import android.app.NativeActivity
|
||||
import android.content.Intent
|
||||
|
||||
class SlintActivity : NativeActivity() {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("gopher64")
|
||||
}
|
||||
}
|
||||
private external fun nativeOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
nativeOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#419567</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1,12 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
|
||||
toolchainVersion=21
|
||||
@@ -0,0 +1,20 @@
|
||||
[versions]
|
||||
agp = "9.2.1"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.14.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#Thu May 21 08:40:22 CEST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Gopher64"
|
||||
include(":app")
|
||||
@@ -2,10 +2,13 @@ fn main() {
|
||||
println!("cargo::rerun-if-changed=parallel-rdp");
|
||||
println!("cargo::rerun-if-changed=retroachievements");
|
||||
println!("cargo::rerun-if-changed=src/compat");
|
||||
println!("cargo::rerun-if-changed=data/translations");
|
||||
println!("cargo::rerun-if-changed=data/ui");
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let slint_config = slint_build::CompilerConfiguration::new();
|
||||
let slint_config = slint_build::CompilerConfiguration::new()
|
||||
.with_bundled_translations("data/translations");
|
||||
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
|
||||
}
|
||||
|
||||
@@ -133,9 +136,6 @@ fn main() {
|
||||
.compile()
|
||||
.unwrap();
|
||||
} else if os == "macos" {
|
||||
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/freetype/lib");
|
||||
println!("cargo:rustc-link-lib=dylib=freetype");
|
||||
|
||||
let output = std::process::Command::new("clang")
|
||||
.args(["--print-runtime-dir"])
|
||||
.output()
|
||||
@@ -146,9 +146,6 @@ fn main() {
|
||||
println!("cargo:rustc-link-search=native={}", runtime_dir);
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.osx");
|
||||
}
|
||||
if cfg!(not(feature = "gui")) && os == "linux" {
|
||||
println!("cargo:rustc-link-lib=dylib=freetype");
|
||||
}
|
||||
|
||||
volk_build.flag("-flto=thin");
|
||||
rdp_build.flag("-flto=thin");
|
||||
@@ -169,7 +166,7 @@ fn main() {
|
||||
.allowlist_function("ra_get_hardcore")
|
||||
.allowlist_function("ra_load_game")
|
||||
.allowlist_function("ra_unload_game")
|
||||
.allowlist_function("ra_set_dmem")
|
||||
.allowlist_function("ra_set_rdram")
|
||||
.allowlist_function("ra_do_frame")
|
||||
.allowlist_function("ra_do_idle")
|
||||
.allowlist_function("ra_http_callback")
|
||||
@@ -177,13 +174,13 @@ fn main() {
|
||||
.allowlist_function("ra_login_token_user")
|
||||
.allowlist_function("ra_state_size")
|
||||
.allowlist_function("ra_save_state")
|
||||
.allowlist_function("ra_load_state");
|
||||
.allowlist_function("ra_load_state")
|
||||
.allowlist_function("ra_get_rich_presence");
|
||||
|
||||
if cfg!(feature = "gui") {
|
||||
retroachievements_builder = retroachievements_builder
|
||||
.allowlist_function("ra_logout_user")
|
||||
.allowlist_function("ra_get_username")
|
||||
.allowlist_function("ra_get_token");
|
||||
.allowlist_function("ra_get_username");
|
||||
}
|
||||
|
||||
let retroachievements_bindings = retroachievements_builder
|
||||
@@ -205,12 +202,13 @@ fn main() {
|
||||
.allowlist_function("rdp_process_commands")
|
||||
.allowlist_function("rdp_onscreen_message")
|
||||
.allowlist_function("rdp_check_callback")
|
||||
.allowlist_function("rdp_new_processor")
|
||||
.allowlist_function("rdp_check_framebuffers")
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
.allowlist_function("rdp_idle")
|
||||
.allowlist_function("rdp_set_fps")
|
||||
.allowlist_function("get_joystick_event")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
@@ -308,4 +306,29 @@ fn main() {
|
||||
{
|
||||
println!("cargo:rustc-cfg=ra_hardcore_enabled");
|
||||
}
|
||||
if os == "android" {
|
||||
copy_dir_all(
|
||||
std::path::Path::new(sdl3_src::SOURCE_DIR)
|
||||
.join("android-project/app/src/main/java/org/libsdl/app"),
|
||||
"android-project/app/src/main/java/org/libsdl/app",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_dir_all(
|
||||
src: impl AsRef<std::path::Path>,
|
||||
dst: impl AsRef<std::path::Path>,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(&dst)?;
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
if ty.is_dir() {
|
||||
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
} else {
|
||||
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,26 @@
|
||||
# Adding translations to Gopher64
|
||||
1. Copy and modify the `gopher64.pot` file in this directory with your translations.
|
||||
2. Rename it to `gopher64.po` and put it here:
|
||||
`data/translations/<lang>/LC_MESSAGES/gopher64.po`
|
||||
|
||||
See [here](https://docs.slint.dev/latest/docs/slint/guide/development/translations/#translating-strings) for more details on creating a .po file. I recommend using a program like [poedit](https://poedit.com) to create the .po file from the template.
|
||||
|
||||
---
|
||||
|
||||
For example, in the .pot file you will see:
|
||||
```
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr ""
|
||||
```
|
||||
|
||||
A translation will look something like this:
|
||||
```
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nova versão disponível! Clique aqui para baixar"
|
||||
```
|
||||
|
||||
The only line you modify is the `msgstr`. The `msgctxt` and `msgid` need to remain unchanged.
|
||||
@@ -0,0 +1,620 @@
|
||||
# gopher64 - German Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Neue Version verfügbar! Zum Herunterladen hier klicken"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discord-Server"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Quellcode"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Spenden (über Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Spenden (über GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokales Spiel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controller-Konfiguration"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats für alle ROMs löschen"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Keine Cheats verfügbar"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Controller-Konfiguration"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Eingabeprofil konfigurieren"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Controller-Einstellungen"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Controller-Zuweisung"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Eingabeprofil erstellen/bearbeiten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Profilname zum Erstellen oder Bearbeiten eingeben"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Deadzone Größe in Prozent"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInput verwenden"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Assistent zur Profilerstellung starten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokales Spiel"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROM öffnen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Speicherordner öffnen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Zuletzt verwendete ROMs"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sitzungsname eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Passwort (optional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sitzungspasswort eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spielername:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spielername eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Für dieses Spiel sind Cheats aktiviert. Cheats werden mit anderen Spielern "
|
||||
"synchronisiert."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Spielername (erforderlich):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spielername eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Passwort (falls erforderlich)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sitzungsliste wird aktualisiert, bitte warten …"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Auf eine Sitzung klicken um diese auszuwählen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Keine Sitzungen verfügbar. Bitte aktualisieren, um erneut zu prüfen."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sitzungsname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spielname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sitzungsliste aktualisieren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discord-Kanal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Warteraum"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Spielname:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Spieler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Spieler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Spieler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Spieler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatnachricht eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Nachricht senden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Eine zu niedrige Eingabeverzögerung führt zu Ruckeln im Spiel.\n"
|
||||
"Sie können die Eingabeverzögerung im Spiel mit Alt+[ verringern und mit "
|
||||
"Alt+] erhöhen."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Lokale Eingabeverzögerung:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Empfohlen: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spiel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Warten auf P2P-Mesh "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Derzeit nicht angemeldet"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Hardcore-Modus"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Challenge Tipps"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Bestenlisten-Tracker"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence für Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Profil anzeigen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Unterstützte Spiele anzeigen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Spiel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Auflösung:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-Herunterskalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Pixelgenaue Skalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Vollbild"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "16:9 (gestreckt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-Shader anwenden"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU übertakten (kann Fehler verursachen)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak deaktivieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Rewind aktivieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emulieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emulieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Benutzeroberfläche"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Dunkel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Hell"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Was möchten Sie sagen?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Server:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Eigene Server-URL eingeben"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "Eigene Server-URL (optional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Server"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Server wird erstellt, bitte warten … Dies kann etwa 30 Sekunden dauern."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Port"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controller"
|
||||
@@ -0,0 +1,620 @@
|
||||
# gopher64 - Spanish Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: Gemini\n"
|
||||
"Language-Team: Spanish (Spain)\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "¡Nueva versión disponible! Pulsa aquí para descargar"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Servidor de Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Código fuente"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Donar (a través de Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Donar (a través de GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Juego local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Config. del mando"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Borrar trucos de todas las ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "No hay trucos disponibles"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Activado"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configuración del mando"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurar perfil de entrada"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Ajustes del mando"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Asignación del mando"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Crear/Modificar perfil de entrada"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Introduce el nombre del perfil para crear o modificar"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Porcentaje de zona muerta"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Usar DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Iniciar asistente de creación de perfil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Juego local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Abrir ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Abrir carpeta de guardados"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs recientes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Introduce el nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Contraseña (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Introduce la contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nombre del jugador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Introduce tu nombre"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Este juego tiene trucos activados. Los trucos se sincronizarán con los otros "
|
||||
"jugadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nombre de jugador (requerido):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Introduce tu nombre"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Contraseña (si es necesaria)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualizando lista de sesiones, espera..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Pulsa una sesión para seleccionarla"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "No hay sesiones disponibles. Actualiza para comprobar de nuevo."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nombre del juego"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualizar lista"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Discord de Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Enviar comentarios"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Sala de espera"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nombre del juego:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jugador 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jugador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jugador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jugador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escribe un mensaje en el chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar mensaje"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Configurar un retardo de entrada demasiado bajo hará que el juego se "
|
||||
"ralentice.\n"
|
||||
"Puedes reducir el retardo de entrada en el juego pulsando Alt+[ y aumentarlo "
|
||||
"con Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Retardo de entrada local:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recomendado: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar juego"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Esperando malla P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Sesión no iniciada"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Activado"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Modo Hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicadores de desafío"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Indicadores de tabla de clasificación"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence en Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Ver perfil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Usuario"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver juegos compatibles"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Juego"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolución:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Reducción de escala SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Escalado por enteros"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Pantalla completa"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Pantalla ancha (estirado)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Sincronización vertical (VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar sombreador CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Overclock CPU N64 (puede causar errores)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desactivar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Activar rebobinado"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interfaz"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Tema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Oscuro"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "¿Qué te gustaría decir?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Servidor:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Introduce la URL de servidor personalizado"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL de servidor personalizado (opcional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Servidor"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Latencia (Ping):"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr "Creando servidor, espera... Puede tardar unos 30 segundos."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Puerto"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Mando"
|
||||
@@ -0,0 +1,622 @@
|
||||
# gopher64 - French (France) Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French (France)\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nouvelle version disponible ! Cliquez ici pour télécharger"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Serveur Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Code source"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Faire un don (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Faire un don (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jeu local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configuration manette"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Effacer la triche pour toutes les ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Aucune triche disponible"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configuration de la manette"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurer le profil d'entrée"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Paramètres de la manette"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Attribution de la manette"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Créer / modifier le profil d'entrée"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Saisir le nom du profil à créer ou à modifier"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Pourcentage de zone morte"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Utiliser DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Lancer l'assistant de création de profil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Jeu local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Ouvrir une ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Ouvrir le dossier des sauvegardes"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs récentes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay : créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Saisir le nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Mot de passe (facultatif) :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Saisir le mot de passe de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nom du joueur :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Saisir le nom du joueur"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"La triche est activée pour ce jeu. Elle sera synchronisée avec les autres "
|
||||
"joueurs."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay : rejoindre une session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nom du joueur (obligatoire) :"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Saisir le nom du joueur"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Mot de passe (si requis)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualisation de la liste des sessions, veuillez patienter…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Cliquez sur une session ci-dessous pour la sélectionner"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
"Aucune session disponible. Veuillez actualiser pour vérifier à nouveau."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nom du jeu"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualiser la liste"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Salon Discord Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Donner votre avis"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay : salle d'attente"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nom du jeu :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Joueur 1 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Joueur 2 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Joueur 3 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Joueur 4 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Saisir un message"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Envoyer"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Régler le délai d'entrée trop bas provoquera des ralentissements.\n"
|
||||
"Vous pouvez diminuer le délai d'entrée en jeu avec Alt+[ et l'augmenter avec "
|
||||
"Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Délai d'entrée local :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recommandé : "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Lancer la partie"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "En attente du maillage P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Non connecté"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Mode hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicateurs de défi"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Suivi des classements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Voir le profil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Nom d'utilisateur"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Voir les jeux pris en charge"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Jeu"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Résolution :"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Réduction SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Mise à l'échelle par entiers"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Plein écran"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Écran large (étiré)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Synchro verticale"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Appliquer le filtre CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Surcharger le CPU N64 (risque de bugs)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Désactiver l'Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Activer le rembobinage"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Émuler UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Émuler le VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thème :"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Système"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Sombre"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Que souhaitez-vous dire ?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Serveur :"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Saisir l'URL personnalisée du serveur"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL personnalisée du serveur (facultatif) :"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Serveur"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping :"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Création du serveur, veuillez patienter… Cela peut prendre environ 30 "
|
||||
#~ "secondes."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Port"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Manette"
|
||||
@@ -0,0 +1,581 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\nYou can lower "
|
||||
"the input delay in-game by pressing Alt+[ and raise it with Alt+]."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,618 @@
|
||||
# gopher64 - Japanese Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Language: ja\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "新しいバージョンがあります。クリックしてダウンロード"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "バージョン情報"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discordサーバー"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "ソースコード"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Patreonで寄付"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "GitHubで寄付"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "ローカルゲーム"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "コントローラー設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "バージョン情報"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "すべてのROMのチートをクリア"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "利用できるチートはありません"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "有効"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "プロファイル"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "コントローラー設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "入力プロファイルを設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "コントローラー詳細設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "コントローラー割り当て"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "入力プロファイルの作成/変更"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "作成または変更するプロファイル名を入力"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "デッドゾーン(%)"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInputを使用"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "プロファイル作成ウィザードを開始"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "ローカルゲーム"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROMを開く"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "セーブフォルダーを開く"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "最近使ったROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "ネットプレイ:セッション作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "セッション名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "パスワード(任意):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "セッションのパスワードを入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "プレイヤー名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "プレイヤー名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr "このゲームではチートが有効です。チートは他のプレイヤーと同期されます。"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "ネットプレイ:セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "プレイヤー名(必須):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "プレイヤー名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "パスワード(必要な場合)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "セッション一覧を更新しています。しばらくお待ちください…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "下の一覧からセッションをクリックして選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "利用できるセッションがありません。更新して再度確認してください。"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "セッション名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "ゲーム名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "セッション一覧を更新"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "ネットプレイ用Discordチャンネル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "フィードバックを送る"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "ネットプレイ:待機室"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "ゲーム名:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "プレイヤー1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "プレイヤー2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "プレイヤー3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "プレイヤー4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "チャットメッセージを入力"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "送信"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"入力遅延を低く設定しすぎるとゲームが遅延します。\n"
|
||||
"Alt+[ でゲーム内の入力遅延を下げ、Alt+] で上げることができます。"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "ローカル入力遅延:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "推奨: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "ゲーム開始"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "P2Pメッシュ接続待機中 "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "ログインしていません"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "有効"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "ハードコアモード"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "チャレンジ表示"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "リーダーボード追跡"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Discord のリッチプレゼンス"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "プロフィールを表示"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "ユーザー名"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "ログイン"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "対応ゲームを表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "ゲーム"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "解像度:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAAダウンスケール"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "整数倍スケーリング"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "全画面表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "ワイド画面(伸ばす)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "垂直同期(VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRTシェーダーを適用"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "拡張パックを無効にする"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "巻き戻しを有効にする"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoaderをエミュレート"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRUをエミュレート"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "UI"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "テーマ:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "システム"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "ダーク"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "ライト"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "何と言いますか?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "サーバー:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "カスタムサーバーURLを入力"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "カスタムサーバーURL(任意):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "サーバー"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "サーバーを作成しています。しばらくお待ちください… 最大約30秒かかることがあ"
|
||||
#~ "ります。"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "ポート"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "コントローラー"
|
||||
@@ -0,0 +1,620 @@
|
||||
# gopher64 - Dutch (Netherlands) Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch (Netherlands)\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nieuwe versie beschikbaar! Klik hier om te downloaden"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discord-server"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Broncode"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Doneren (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doneren (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokaal spel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controllerconfiguratie"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats wissen voor alle ROM's"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Geen cheats beschikbaar"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Ingeschakeld"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profiel"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Controllerconfiguratie"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Invoerprofiel configureren"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Controllerinstellingen"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Controllertoewijzing"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Invoerprofiel maken/wijzigen"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Profielnaam invoeren om aan te maken of te wijzigen"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Dode-zonepercentage"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInput gebruiken"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Wizard voor profiel maken starten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokaal spel"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROM openen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Map met opslagen openen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Recent gebruikte ROM's"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sessienaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Wachtwoord (optioneel):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sessiewachtwoord invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spelernaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spelernaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Voor dit spel zijn cheats ingeschakeld. Cheats worden gesynchroniseerd met "
|
||||
"andere spelers."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Spelernaam (verplicht):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spelernaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Wachtwoord (indien vereist)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sessielijst wordt vernieuwd, even geduld…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Klik hieronder op een sessie om die te selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Geen sessies beschikbaar. Vernieuw om opnieuw te controleren."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sessienaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spelnaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sessielijst vernieuwen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discordkanaal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geven"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: wachtruimte"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Spelnaam:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Speler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Speler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Speler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Speler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatbericht invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Bericht verzenden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Een te lage invoervertraging zorgt ervoor dat het spel gaat haperen.\n"
|
||||
"Je kunt de invoervertraging in het spel verlagen met Alt+[ en verhogen met "
|
||||
"Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Lokale invoervertraging:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Aanbevolen: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Wachten op P2P-mesh "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Momenteel niet ingelogd"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Ingeschakeld"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Hardcore-modus"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Challenge-indicatoren"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Leaderboard-trackers"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Discord Rich Presence"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Profiel bekijken"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Gebruikersnaam"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Inloggen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ondersteunde spellen bekijken"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Spel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolutie:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-downscaling"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Schalen in hele getallen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Volledig scherm"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Breedbeeld (uitgerekt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-shader toepassen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU overklokken (kan bugs veroorzaken)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak uitschakelen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Terugspoelen inschakelen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emuleren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emuleren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Systeem"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Donker"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Licht"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Wat wil je zeggen?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Server:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Aangepaste server-URL invoeren"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "Aangepaste server-URL (optioneel):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Server"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Server wordt aangemaakt, even geduld… Dit kan ongeveer 30 seconden duren."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Poort"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controller"
|
||||
@@ -0,0 +1,621 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 18:11-3000\n"
|
||||
"Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: Brazilian Portuguese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nova versão disponível! Clique aqui para baixar"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Servidor do Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Código Fonte"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Doações (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doações (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jogo Local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configurar Controle"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Limpar Trapaças para todas as ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Sem trapaças disponíveis"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Habilitado"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configurar Controles"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurar Perfil de Controle"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Configurações do controle"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Atribuição do controle"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Criar/Modificar Perfil de Comandos"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Insira o nome de perfil para criar ou modificar"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Porcentagem de Zona-morta"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Usar DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Iniciar Assistente de Criação de Perfil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Jogo Local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Abrir ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Abrir Pasta de Saves"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs Recentes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Insira nome da sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Senha: (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Insira a senha da sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nome de Jogador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Insira nome de jogador"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Este jogo tem trapaças habilitadas. As trapaças serão sincronizadas com "
|
||||
"outros jogadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Entrar na sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nome de Jogador (obrigatório):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Insira o nome de jogador"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Senha (caso obrigatória)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Recarregando lista de sessões, por favor aguarde..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Clique em uma sessão abaixo para selecionar"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
"Sem sessões disponíveis. Por favor recarregue para verificar novamente."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nome da Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nome do Jogo"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Recarregar Lista de Sessões"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Netplay do Discord"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Fornecer Feedback"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Aguardando Sala"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nome do Jogo:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jogador 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jogador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jogador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jogador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escrever mensagem no chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar Mensagem"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Definir o atraso de entrada muito baixo causará lag no jogo.\n"
|
||||
"Você pode diminuir o atraso de entrada no jogo pressionando Alt+[ e aumentá-"
|
||||
"lo com Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Atraso de entrada local:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recomendado: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar jogo"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Aguardando malha P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Não está logado."
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Habilitado"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Modo Hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicadores de Desafio"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Rastreadores de Tabela"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence no Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Ver Perfil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Usuário"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver Jogos com Suporte"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Jogo"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolução:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Redução de SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Proporção Íntegra"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Tela Cheia"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Widescreen (esticar)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar filtro de CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Overclock da CPU do N64 (pode causar bugs)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desabilitar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Habilitar rebobinamento"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Tema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Escuro"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "O que você quer falar?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Servidor:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Insira a URL personalizada do servidor"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL Personalizada de Servidor (opcional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Servidor"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Criando servidor, por favor aguarde...Isto poderá levar até 30 segundos."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Entrada"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controle"
|
||||
@@ -0,0 +1,621 @@
|
||||
# gopher64 - Russian Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Доступна новая версия! Нажмите здесь для загрузки"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "О программе"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Вики"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Сервер Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Исходный код"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Пожертвовать (через Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Пожертвовать (через GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Локальная игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Настройка контроллера"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "О программе"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Очистить читы для всех ROMов"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Читы недоступны"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Профиль"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Настройка контроллера"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Настроить профиль ввода"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Настройки контроллера"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Назначение контроллера"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Создать/изменить профиль ввода"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Введите имя профиля для создания или изменения"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Процент мёртвой зоны"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Использовать DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Запустить мастер создания профиля"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Локальная игра"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Открыть ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Открыть папку сохранений"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Недавние ROMы"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Сетевая игра: Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Введите имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Пароль (необязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Введите пароль сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Имя игрока:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Введите имя игрока"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Для этой игры включены читы. Читы будут синхронизированы с другими игроками."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Сетевая игра: Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Имя игрока (обязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Введите имя игрока"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Пароль (если требуется)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Обновление списка сессий, пожалуйста, подождите..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Нажмите на сессию ниже, чтобы выбрать её"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Нет доступных сессий. Пожалуйста, обновите, чтобы проверить снова."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Название игры"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Обновить список сессий"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Канал Discord для сетевой игры"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Оставить отзыв"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Сетевая игра: Комната ожидания"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Название игры:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Игрок 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Игрок 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Игрок 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Игрок 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Введите сообщение чата"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Отправить сообщение"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Слишком низкая задержка ввода вызовет лаги в игре.\n"
|
||||
"Вы можете уменьшить задержку ввода в игре, нажав Alt+[, и увеличить её с "
|
||||
"помощью Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Локальная задержка ввода:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Рекомендуется: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Начать игру"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Ожидание P2P-сети "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "В данный момент не выполнен вход"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Хардкорный режим"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Индикаторы испытаний"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Трекеры таблицы лидеров"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Расширенный статус Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Просмотреть профиль"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Имя пользователя"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Войти"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Просмотреть поддерживаемые игры"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Игра"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Разрешение:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA даунскейлинг"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Целочисленное масштабирование"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Полноэкранный режим"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Широкоэкранный режим (растянуто)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Вертикальная синхронизация"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Применить шейдер ЭЛТ"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Разгон процессора N64 (может вызвать ошибки)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Отключить Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Включить перемотку назад"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Эмулировать UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Эмулировать VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Интерфейс"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Тема:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Тёмная"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Что вы хотите сказать?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Сервер:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Введите URL пользовательского сервера"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL пользовательского сервера (необязательно):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Сервер"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Пинг:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Создание сервера, пожалуйста, подождите... Это может занять около 30 "
|
||||
#~ "секунд."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Порт"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Контроллер"
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 553 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M280-120v-80h160v-124q-49-11-87.5-41.5T296-442q-75-9-125.5-65.5T120-640v-40q0-33 23.5-56.5T200-760h80v-80h400v80h80q33 0 56.5 23.5T840-680v40q0 76-50.5 132.5T664-442q-18 46-56.5 76.5T520-324v124h160v80H280Zm0-408v-152h-80v40q0 38 22 68.5t58 43.5Zm285 93q35-35 35-85v-240H360v240q0 50 35 85t85 35q50 0 85-35Zm115-93q36-13 58-43.5t22-68.5v-40h-80v152Zm-200-52Z"/></svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160v-80h320v80H480ZM220-320l-56-56 183-184-183-184 56-56 240 240-240 240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 203 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M189-160q-60 0-102.5-43T42-307q0-9 1-18t3-18l84-336q14-54 57-87.5t98-33.5h390q55 0 98 33.5t57 87.5l84 336q2 9 3.5 18.5T919-306q0 61-43.5 103.5T771-160q-42 0-78-22t-54-60l-28-58q-5-10-15-15t-21-5H385q-11 0-21 5t-15 15l-28 58q-18 38-54 60t-78 22Zm3-80q19 0 34.5-10t23.5-27l28-57q15-31 44-48.5t63-17.5h190q34 0 63 18t45 48l28 57q8 17 23.5 27t34.5 10q28 0 48-18.5t21-46.5q0 1-2-19l-84-335q-7-27-28-44t-49-17H285q-28 0-49.5 17T208-659l-84 335q-2 6-2 18 0 28 20.5 47t49.5 19Zm376.5-291.5Q580-543 580-560t-11.5-28.5Q557-600 540-600t-28.5 11.5Q500-577 500-560t11.5 28.5Q523-520 540-520t28.5-11.5Zm80-80Q660-623 660-640t-11.5-28.5Q637-680 620-680t-28.5 11.5Q580-657 580-640t11.5 28.5Q603-600 620-600t28.5-11.5Zm0 160Q660-463 660-480t-11.5-28.5Q637-520 620-520t-28.5 11.5Q580-497 580-480t11.5 28.5Q603-440 620-440t28.5-11.5Zm80-80Q740-543 740-560t-11.5-28.5Q717-600 700-600t-28.5 11.5Q660-577 660-560t11.5 28.5Q683-520 700-520t28.5-11.5Zm-367 63Q370-477 370-490v-40h40q13 0 21.5-8.5T440-560q0-13-8.5-21.5T410-590h-40v-40q0-13-8.5-21.5T340-660q-13 0-21.5 8.5T310-630v40h-40q-13 0-21.5 8.5T240-560q0 13 8.5 21.5T270-530h40v40q0 13 8.5 21.5T340-460q13 0 21.5-8.5ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H260Zm0-80h480q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm220-240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -54,6 +54,14 @@ enum vi_registers {
|
||||
VI_REGS_COUNT
|
||||
};
|
||||
|
||||
enum user_event_codes {
|
||||
USER_EVENT_SAVE_STATE = 1,
|
||||
USER_EVENT_LOAD_STATE = 2,
|
||||
USER_EVENT_EXIT_GAME = 3,
|
||||
USER_EVENT_FAST_FORWARD = 4,
|
||||
USER_EVENT_LOAD_REWIND = 5,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint32_t depthbuffer_address;
|
||||
uint32_t framebuffer_address;
|
||||
@@ -75,6 +83,9 @@ typedef struct {
|
||||
FrameBufferInfo frame_buffer_info;
|
||||
} RDP_DEVICE;
|
||||
|
||||
static void *g_tmem = nullptr;
|
||||
static void *g_hidden_rdram = nullptr;
|
||||
|
||||
typedef struct {
|
||||
uint32_t fps;
|
||||
uint32_t vis;
|
||||
@@ -82,6 +93,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
std::string message;
|
||||
uint64_t milliseconds;
|
||||
Vulkan::ImageHandle image;
|
||||
} Message;
|
||||
|
||||
@@ -97,12 +109,11 @@ static GFX_INFO gfx_info;
|
||||
static const uint32_t *fragment_spirv;
|
||||
static size_t fragment_size;
|
||||
|
||||
std::vector<bool> rdram_dirty;
|
||||
uint64_t sync_signal;
|
||||
static std::vector<bool> rdram_dirty;
|
||||
static uint64_t sync_signal;
|
||||
|
||||
static TTF_Font *message_font;
|
||||
static std::queue<Message> messages;
|
||||
static uint64_t message_timer;
|
||||
|
||||
static TTF_Font *achievement_challenge_indicator_font;
|
||||
static std::vector<const char *> achievement_challenge_indicators;
|
||||
@@ -114,7 +125,7 @@ static bool display_challenge_indicator;
|
||||
static bool display_fps;
|
||||
static Vulkan::ImageHandle fps_image;
|
||||
|
||||
#define MESSAGE_TIME 3000 // 3 seconds
|
||||
static std::queue<JoystickEvent> joystick_events;
|
||||
|
||||
typedef struct {
|
||||
float SourceSize[4];
|
||||
@@ -127,6 +138,12 @@ static const unsigned cmd_len_lut[64] = {
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
};
|
||||
|
||||
static void add_joystick_event(void *userdata) {
|
||||
JoystickEvent *joystick_event = (JoystickEvent *)userdata;
|
||||
joystick_events.push(*joystick_event);
|
||||
delete joystick_event;
|
||||
}
|
||||
|
||||
bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
callback.paused = false;
|
||||
@@ -134,6 +151,7 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
} else if (event->type == SDL_EVENT_WINDOW_RESIZED && callback.emu_running) {
|
||||
wsi_platform->do_resize();
|
||||
} else if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat) {
|
||||
SDL_Event user_event;
|
||||
switch (event->key.scancode) {
|
||||
case SDL_SCANCODE_RETURN:
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
@@ -143,7 +161,10 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
break;
|
||||
case SDL_SCANCODE_F:
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.enable_speedlimiter = !callback.enable_speedlimiter;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_FAST_FORWARD;
|
||||
SDL_PushEvent(&user_event);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_P:
|
||||
@@ -151,9 +172,14 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
callback.paused = !callback.paused;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_AC_BACK:
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
if (gfx_info.fullscreen)
|
||||
callback.emu_running = false;
|
||||
if (gfx_info.fullscreen) {
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_EXIT_GAME;
|
||||
SDL_PushEvent(&user_event);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_F1:
|
||||
display_fps = !display_fps;
|
||||
@@ -162,14 +188,26 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
crop_letterbox = !crop_letterbox;
|
||||
break;
|
||||
case SDL_SCANCODE_F5:
|
||||
callback.save_state = true;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_SAVE_STATE;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F6:
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_LOAD_REWIND;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F7:
|
||||
callback.load_state = true;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_LOAD_STATE;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F8:
|
||||
if (messages.empty())
|
||||
SDL_RunOnMainThread(ra_display_inprogress_achievements, nullptr, true);
|
||||
SDL_RunOnMainThread(ra_display_inprogress_achievements, nullptr, false);
|
||||
break;
|
||||
case SDL_SCANCODE_F9:
|
||||
display_challenge_indicator = !display_challenge_indicator;
|
||||
@@ -177,16 +215,24 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
std::format("Challenge indicators: {}",
|
||||
display_challenge_indicator ? "ON" : "OFF")
|
||||
.c_str(),
|
||||
false);
|
||||
MESSAGE_VERY_SHORT);
|
||||
break;
|
||||
case SDL_SCANCODE_F12:
|
||||
callback.reset_game = true;
|
||||
break;
|
||||
case SDL_SCANCODE_LEFTBRACKET:
|
||||
callback.lower_volume = true;
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.decrease_input_delay = true;
|
||||
} else {
|
||||
callback.lower_volume = true;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHTBRACKET:
|
||||
callback.raise_volume = true;
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.increase_input_delay = true;
|
||||
} else {
|
||||
callback.raise_volume = true;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_SLASH:
|
||||
callback.frame_advance = true;
|
||||
@@ -211,21 +257,65 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event->type == SDL_EVENT_USER) {
|
||||
switch (event->user.code) {
|
||||
case USER_EVENT_SAVE_STATE:
|
||||
callback.save_state = true;
|
||||
break;
|
||||
case USER_EVENT_LOAD_REWIND:
|
||||
callback.load_rewind = true;
|
||||
break;
|
||||
case USER_EVENT_LOAD_STATE:
|
||||
callback.load_state = true;
|
||||
break;
|
||||
case USER_EVENT_EXIT_GAME:
|
||||
callback.emu_running = false;
|
||||
break;
|
||||
case USER_EVENT_FAST_FORWARD:
|
||||
callback.enable_speedlimiter = !callback.enable_speedlimiter;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event->type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||
JoystickEvent *joystick_event = new JoystickEvent;
|
||||
joystick_event->joystick_id = event->jdevice.which;
|
||||
joystick_event->connected = true;
|
||||
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
|
||||
} else if (event->type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
JoystickEvent *joystick_event = new JoystickEvent;
|
||||
joystick_event->joystick_id = event->jdevice.which;
|
||||
joystick_event->connected = false;
|
||||
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
|
||||
} else if (event->type == SDL_EVENT_WILL_ENTER_BACKGROUND) {
|
||||
wsi->end_frame();
|
||||
wsi->deinit_surface_and_swapchain();
|
||||
} else if (event->type == SDL_EVENT_RENDER_DEVICE_RESET) {
|
||||
wsi->init_surface_swapchain();
|
||||
wsi->begin_frame();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void rdp_new_processor(GFX_INFO _gfx_info) {
|
||||
gfx_info = _gfx_info;
|
||||
JoystickEvent get_joystick_event() {
|
||||
if (joystick_events.empty())
|
||||
return JoystickEvent{0, false};
|
||||
JoystickEvent joystick_event = joystick_events.front();
|
||||
joystick_events.pop();
|
||||
return joystick_event;
|
||||
}
|
||||
|
||||
void rdp_idle() {
|
||||
processor->idle();
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
}
|
||||
|
||||
if (processor) {
|
||||
delete processor;
|
||||
}
|
||||
RDP::CommandProcessorFlags flags = 0;
|
||||
static void rdp_new_processor() {
|
||||
RDP::CommandProcessorFlags flags =
|
||||
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_HIDDEN_RDRAM_BIT |
|
||||
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_TMEM_BIT;
|
||||
|
||||
if (gfx_info.upscale == 2) {
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
@@ -236,11 +326,24 @@ void rdp_new_processor(GFX_INFO _gfx_info) {
|
||||
} else if (gfx_info.upscale == 8) {
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT;
|
||||
} else {
|
||||
gfx_info.upscale = 1;
|
||||
}
|
||||
|
||||
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0,
|
||||
gfx_info.RDRAM_SIZE,
|
||||
gfx_info.RDRAM_SIZE / 2, flags);
|
||||
g_tmem = processor->get_tmem();
|
||||
if (!g_tmem) {
|
||||
LOGE("Failed to get tmem\n");
|
||||
}
|
||||
g_hidden_rdram = processor->begin_read_hidden_rdram();
|
||||
if (!g_hidden_rdram) {
|
||||
LOGE("Failed to get hidden_rdram\n");
|
||||
}
|
||||
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
}
|
||||
|
||||
static ImageHandle create_message_image(Vulkan::Device &device, int width,
|
||||
@@ -267,12 +370,13 @@ static ImageHandle create_message_image(Vulkan::Device &device, int width,
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
size_t font_size, uint32_t save_state_slot) {
|
||||
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
|
||||
memset(&callback, 0, sizeof(CALL_BACK));
|
||||
|
||||
window = (SDL_Window *)_window;
|
||||
SDL_SyncWindow(window);
|
||||
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
|
||||
if (!result) {
|
||||
printf("Could not add event watch.\n");
|
||||
LOGE("Could not add event watch.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -309,7 +413,7 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
return;
|
||||
}
|
||||
|
||||
rdp_new_processor(gfx_info);
|
||||
rdp_new_processor();
|
||||
|
||||
if (!processor->device_is_supported()) {
|
||||
rdp_close();
|
||||
@@ -334,7 +438,6 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
crop_letterbox = false;
|
||||
|
||||
messages = std::queue<Message>();
|
||||
message_timer = 0;
|
||||
|
||||
display_challenge_indicator = true;
|
||||
achievement_challenge_indicators.clear();
|
||||
@@ -346,6 +449,11 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
}
|
||||
|
||||
void rdp_close() {
|
||||
display_fps = false;
|
||||
|
||||
g_tmem = nullptr;
|
||||
g_hidden_rdram = nullptr;
|
||||
|
||||
messages = std::queue<Message>();
|
||||
achievement_challenge_indicator_image = Vulkan::ImageHandle();
|
||||
achievement_progress_indicator_image = Vulkan::ImageHandle();
|
||||
@@ -374,6 +482,8 @@ void rdp_close() {
|
||||
delete wsi_platform;
|
||||
wsi_platform = nullptr;
|
||||
}
|
||||
|
||||
SDL_RemoveEventWatch(sdl_event_filter, nullptr);
|
||||
}
|
||||
|
||||
static void calculate_viewport(float *x, float *y, float *width, float *height,
|
||||
@@ -444,11 +554,34 @@ static void draw_fps(CommandBufferHandle cmd, VkViewport vp) {
|
||||
cmd->draw(3);
|
||||
}
|
||||
|
||||
static void pop_message(void *userdata) { messages.pop(); }
|
||||
static uint32_t pop_message_callback(void *userdata, SDL_TimerID timerID,
|
||||
uint32_t interval) {
|
||||
SDL_RunOnMainThread(pop_message, NULL, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void render_frame(Vulkan::Device &device) {
|
||||
RDP::ScanoutOptions options = {};
|
||||
options.persist_frame_on_invalid_input = true;
|
||||
options.blend_previous_frame = true;
|
||||
options.upscale_deinterlacing = false;
|
||||
options.blend_previous_frame = !gfx_info.ssaa;
|
||||
options.upscale_deinterlacing = gfx_info.ssaa;
|
||||
if (gfx_info.ssaa) {
|
||||
switch (gfx_info.upscale) {
|
||||
case 2:
|
||||
options.downscale_steps = 1;
|
||||
break;
|
||||
case 4:
|
||||
options.downscale_steps = 2;
|
||||
break;
|
||||
case 8:
|
||||
options.downscale_steps = 3;
|
||||
break;
|
||||
default:
|
||||
options.downscale_steps = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (crop_letterbox && gfx_info.widescreen) {
|
||||
options.crop_rect.enable = true;
|
||||
@@ -489,10 +622,10 @@ static void render_frame(Vulkan::Device &device) {
|
||||
cmd->set_depth_test(false, false);
|
||||
cmd->set_cull_mode(VK_CULL_MODE_NONE);
|
||||
|
||||
VkViewport vp = cmd->get_viewport();
|
||||
// If we don't have an image, we just get a cleared screen in the render
|
||||
// pass.
|
||||
if (image) {
|
||||
VkViewport vp = cmd->get_viewport();
|
||||
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height,
|
||||
image->get_height() / gfx_info.upscale);
|
||||
|
||||
@@ -513,37 +646,32 @@ static void render_frame(Vulkan::Device &device) {
|
||||
// The vertices are constants in the shader.
|
||||
// Draws fullscreen quad using oversized triangle.
|
||||
cmd->draw(3);
|
||||
|
||||
if (!messages.empty()) {
|
||||
Message *message = &messages.front();
|
||||
if (!message->image) {
|
||||
message->image = create_message_image(device, vp.width, message_font,
|
||||
message->message.c_str());
|
||||
}
|
||||
cmd->set_texture(0, 0, message->image->get_view(),
|
||||
Vulkan::StockSampler::NearestClamp);
|
||||
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
|
||||
vp.y = vp.y + vp.height - message->image->get_height();
|
||||
vp.height = message->image->get_height();
|
||||
vp.width = message->image->get_width();
|
||||
cmd->set_viewport(vp);
|
||||
|
||||
cmd->draw(3);
|
||||
|
||||
if (SDL_GetTicks() > message_timer) {
|
||||
messages.pop();
|
||||
message_timer = SDL_GetTicks() + MESSAGE_TIME;
|
||||
}
|
||||
} else if (achievement_progress_indicator_image) {
|
||||
draw_indicator(cmd, achievement_progress_indicator_image, vp);
|
||||
} else if (achievement_challenge_indicator_image &&
|
||||
display_challenge_indicator) {
|
||||
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
|
||||
}
|
||||
if (!messages.empty()) {
|
||||
Message *message = &messages.front();
|
||||
if (!message->image) {
|
||||
message->image = create_message_image(device, vp.width, message_font,
|
||||
message->message.c_str());
|
||||
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
|
||||
}
|
||||
cmd->set_texture(0, 0, message->image->get_view(),
|
||||
Vulkan::StockSampler::NearestClamp);
|
||||
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
|
||||
vp.y = vp.y + vp.height - message->image->get_height();
|
||||
vp.height = message->image->get_height();
|
||||
vp.width = message->image->get_width();
|
||||
cmd->set_viewport(vp);
|
||||
|
||||
if (messages.empty() && display_fps && fps_image) {
|
||||
draw_fps(cmd, vp);
|
||||
}
|
||||
cmd->draw(3);
|
||||
} else if (achievement_progress_indicator_image) {
|
||||
draw_indicator(cmd, achievement_progress_indicator_image, vp);
|
||||
} else if (achievement_challenge_indicator_image &&
|
||||
display_challenge_indicator) {
|
||||
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
|
||||
}
|
||||
|
||||
if (messages.empty() && display_fps && fps_image) {
|
||||
draw_fps(cmd, vp);
|
||||
}
|
||||
|
||||
cmd->end_render_pass();
|
||||
@@ -561,17 +689,26 @@ void rdp_render_frame() {
|
||||
}
|
||||
|
||||
void rdp_update_screen() {
|
||||
wsi->end_frame();
|
||||
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
|
||||
return;
|
||||
}
|
||||
if (!wsi->end_frame()) {
|
||||
LOGE("End frame failed\n");
|
||||
SDL_PumpEvents(); // For Android to trigger pause event
|
||||
}
|
||||
wsi->begin_frame();
|
||||
}
|
||||
|
||||
CALL_BACK rdp_check_callback() {
|
||||
CALL_BACK return_value = callback;
|
||||
callback.save_state = false;
|
||||
callback.load_rewind = false;
|
||||
callback.load_state = false;
|
||||
callback.reset_game = false;
|
||||
callback.lower_volume = false;
|
||||
callback.raise_volume = false;
|
||||
callback.decrease_input_delay = false;
|
||||
callback.increase_input_delay = false;
|
||||
callback.frame_advance = false;
|
||||
return return_value;
|
||||
}
|
||||
@@ -597,27 +734,49 @@ void rdp_check_framebuffers(uint32_t address, uint32_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
size_t rdp_state_size() { return sizeof(RDP_DEVICE); }
|
||||
size_t rdp_state_size() {
|
||||
return sizeof(RDP_DEVICE) + 0x1000 + processor->get_hidden_rdram_size();
|
||||
}
|
||||
|
||||
void rdp_save_state(uint8_t *state) {
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
|
||||
|
||||
if (g_tmem) {
|
||||
memcpy(state + sizeof(RDP_DEVICE), g_tmem, 0x1000);
|
||||
}
|
||||
|
||||
if (g_hidden_rdram) {
|
||||
memcpy(state + sizeof(RDP_DEVICE) + 0x1000, g_hidden_rdram,
|
||||
processor->get_hidden_rdram_size());
|
||||
}
|
||||
}
|
||||
|
||||
void rdp_load_state(const uint8_t *state) {
|
||||
void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state) {
|
||||
gfx_info = _gfx_info;
|
||||
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
|
||||
|
||||
if (g_tmem) {
|
||||
memcpy(g_tmem, state + sizeof(RDP_DEVICE), 0x1000);
|
||||
}
|
||||
|
||||
if (g_hidden_rdram) {
|
||||
memcpy(g_hidden_rdram, state + sizeof(RDP_DEVICE) + 0x1000,
|
||||
processor->get_hidden_rdram_size());
|
||||
}
|
||||
}
|
||||
|
||||
static void push_onscreen_message(void *message) {
|
||||
if (messages.empty())
|
||||
message_timer = SDL_GetTicks() + MESSAGE_TIME;
|
||||
messages.push({std::string((const char *)message), Vulkan::ImageHandle()});
|
||||
static void push_onscreen_message(void *data) {
|
||||
Message *message = (Message *)data;
|
||||
messages.push(*message);
|
||||
delete message;
|
||||
}
|
||||
|
||||
void rdp_onscreen_message(const char *message, bool long_message) {
|
||||
SDL_RunOnMainThread(push_onscreen_message, (void *)message, true);
|
||||
if (long_message)
|
||||
SDL_RunOnMainThread(push_onscreen_message, (void *)message, true);
|
||||
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds) {
|
||||
Message *data = new Message;
|
||||
data->message = message;
|
||||
data->milliseconds = static_cast<uint64_t>(milliseconds);
|
||||
data->image = Vulkan::ImageHandle();
|
||||
SDL_RunOnMainThread(push_onscreen_message, data, false);
|
||||
}
|
||||
|
||||
uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
|
||||
@@ -631,7 +790,7 @@ uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
|
||||
case 3:
|
||||
return area * 4;
|
||||
default:
|
||||
printf("Invalid pixel size: %u\n", pixel_type);
|
||||
LOGE("Invalid pixel size: %u\n", pixel_type);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -849,17 +1008,24 @@ uint64_t rdp_process_commands() {
|
||||
return interrupt_timer;
|
||||
}
|
||||
|
||||
void rdp_set_fps_callback(void *userdata) {
|
||||
static void rdp_set_fps_callback(void *userdata) {
|
||||
FPS_DATA *data = (FPS_DATA *)userdata;
|
||||
if (!wsi) {
|
||||
delete data;
|
||||
return;
|
||||
}
|
||||
fps_image = create_message_image(
|
||||
wsi->get_device(), 0, achievement_challenge_indicator_font,
|
||||
std::format("FPS: {} VI/S: {}", data->fps, data->vis).c_str());
|
||||
delete data;
|
||||
}
|
||||
|
||||
void rdp_set_fps(uint32_t fps, uint32_t vis) {
|
||||
if (display_fps) {
|
||||
FPS_DATA data = {fps, vis};
|
||||
SDL_RunOnMainThread(rdp_set_fps_callback, &data, true);
|
||||
FPS_DATA *data = new FPS_DATA;
|
||||
data->fps = fps;
|
||||
data->vis = vis;
|
||||
SDL_RunOnMainThread(rdp_set_fps_callback, data, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ typedef struct {
|
||||
bool vsync;
|
||||
bool integer_scaling;
|
||||
uint32_t upscale;
|
||||
bool ssaa;
|
||||
bool crt;
|
||||
} GFX_INFO;
|
||||
|
||||
@@ -28,15 +29,29 @@ typedef struct {
|
||||
bool emu_running;
|
||||
bool save_state;
|
||||
bool load_state;
|
||||
bool load_rewind;
|
||||
bool reset_game;
|
||||
bool enable_speedlimiter;
|
||||
bool lower_volume;
|
||||
bool raise_volume;
|
||||
bool decrease_input_delay;
|
||||
bool increase_input_delay;
|
||||
bool paused;
|
||||
bool frame_advance;
|
||||
uint32_t save_state_slot;
|
||||
} CALL_BACK;
|
||||
|
||||
typedef struct {
|
||||
uint32_t joystick_id;
|
||||
bool connected;
|
||||
} JoystickEvent;
|
||||
|
||||
typedef enum {
|
||||
MESSAGE_VERY_SHORT = 500,
|
||||
MESSAGE_SHORT = 3000,
|
||||
MESSAGE_LONG = 6000,
|
||||
} MESSAGE_LENGTH;
|
||||
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
size_t font_size, uint32_t save_state_slot);
|
||||
void rdp_close();
|
||||
@@ -45,14 +60,16 @@ void rdp_update_screen();
|
||||
void rdp_render_frame();
|
||||
CALL_BACK rdp_check_callback();
|
||||
uint64_t rdp_process_commands();
|
||||
void rdp_onscreen_message(const char *message, bool long_message);
|
||||
void rdp_new_processor(GFX_INFO _gfx_info);
|
||||
void rdp_idle();
|
||||
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds);
|
||||
void rdp_check_framebuffers(uint32_t address, uint32_t length);
|
||||
size_t rdp_state_size();
|
||||
void rdp_save_state(uint8_t *state);
|
||||
void rdp_load_state(const uint8_t *state);
|
||||
void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state);
|
||||
void rdp_set_fps(uint32_t fps, uint32_t vis);
|
||||
|
||||
JoystickEvent get_joystick_event();
|
||||
|
||||
void achievement_challenge_indicator_add(const char *achievement_title);
|
||||
void achievement_challenge_indicator_remove(const char *achievement_title);
|
||||
void achievement_progress_add(const char *achievement_title,
|
||||
|
||||
@@ -46,7 +46,7 @@ uint32_t SDL_WSIPlatform::get_surface_height() {
|
||||
|
||||
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
|
||||
|
||||
void SDL_WSIPlatform::poll_input() {}
|
||||
void SDL_WSIPlatform::poll_input() { SDL_PumpEvents(); }
|
||||
|
||||
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
|
||||
|
||||
|
||||
@@ -16,18 +16,20 @@ void store_retroachievements_credentials(const char *username,
|
||||
void notify_load_game(void *userdata);
|
||||
|
||||
static rc_client_t *g_client = NULL;
|
||||
static const uint8_t *g_dmem = NULL;
|
||||
static size_t g_dmem_size = 0;
|
||||
static const uint8_t *g_rdram = NULL;
|
||||
static size_t g_rdram_size = 0;
|
||||
static bool g_challenge = false;
|
||||
static bool g_leaderboard = false;
|
||||
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
|
||||
|
||||
static char g_rich_presence_buffer[512];
|
||||
|
||||
static uint32_t read_memory(uint32_t address, uint8_t *buffer,
|
||||
uint32_t num_bytes, rc_client_t *client) {
|
||||
if (address + num_bytes > g_dmem_size)
|
||||
if (address + num_bytes > g_rdram_size)
|
||||
memset(buffer, 0, num_bytes);
|
||||
else
|
||||
memcpy(buffer, &g_dmem[address], num_bytes);
|
||||
memcpy(buffer, &g_rdram[address], num_bytes);
|
||||
return num_bytes;
|
||||
}
|
||||
|
||||
@@ -84,10 +86,6 @@ const char *ra_get_username() {
|
||||
const rc_client_user_t *user = rc_client_get_user_info(g_client);
|
||||
return user ? user->username : NULL;
|
||||
}
|
||||
const char *ra_get_token() {
|
||||
const rc_client_user_t *user = rc_client_get_user_info(g_client);
|
||||
return user ? user->token : NULL;
|
||||
}
|
||||
|
||||
void ra_logout_user() { rc_client_logout(g_client); }
|
||||
|
||||
@@ -128,29 +126,40 @@ static void load_game_callback(int result, const char *error_message,
|
||||
notify_load_game(userdata);
|
||||
}
|
||||
|
||||
void ra_welcome() {
|
||||
if (!g_client)
|
||||
void ra_welcome(const char **game_title, const char **game_image_url) {
|
||||
if (!g_client) {
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
|
||||
char *error_message = rc_client_get_userdata(g_client);
|
||||
if (error_message) {
|
||||
snprintf(buffer, sizeof(buffer), "RA Error: %s", error_message);
|
||||
rdp_onscreen_message(buffer, true);
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
free(error_message);
|
||||
rc_client_set_userdata(g_client, NULL);
|
||||
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
const rc_client_game_t *game = rc_client_get_game_info(g_client);
|
||||
if (!game) {
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
rc_client_user_game_summary_t summary;
|
||||
rc_client_get_user_game_summary(g_client, &summary);
|
||||
|
||||
int message_length = snprintf(
|
||||
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title,
|
||||
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Softcore");
|
||||
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Casual");
|
||||
|
||||
if (summary.num_core_achievements != 0) {
|
||||
snprintf(buffer + message_length, sizeof(buffer) - message_length,
|
||||
@@ -160,7 +169,10 @@ void ra_welcome() {
|
||||
snprintf(buffer + message_length, sizeof(buffer) - message_length,
|
||||
"Game has no achievements");
|
||||
}
|
||||
rdp_onscreen_message(buffer, true);
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
|
||||
*game_title = game->title;
|
||||
*game_image_url = game->badge_url;
|
||||
}
|
||||
|
||||
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
|
||||
@@ -177,9 +189,9 @@ void ra_unload_game() {
|
||||
rc_client_unload_game(g_client);
|
||||
}
|
||||
|
||||
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size) {
|
||||
g_dmem = dmem;
|
||||
g_dmem_size = dmem_size;
|
||||
void ra_set_rdram(const uint8_t *rdram, size_t rdram_size) {
|
||||
g_rdram = rdram;
|
||||
g_rdram_size = rdram_size;
|
||||
}
|
||||
|
||||
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
|
||||
@@ -187,14 +199,14 @@ static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s",
|
||||
leaderboard->title, leaderboard->tracker_value);
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void achievement_triggered(const rc_client_achievement_t *achievement) {
|
||||
char buffer[512];
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title);
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void game_completed(rc_client_t *client) {
|
||||
@@ -204,7 +216,7 @@ static void game_completed(rc_client_t *client) {
|
||||
snprintf(buffer, sizeof(buffer), "%s: %s",
|
||||
rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",
|
||||
game->title);
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void subset_completed(const rc_client_subset_t *subset,
|
||||
@@ -214,7 +226,7 @@ static void subset_completed(const rc_client_subset_t *subset,
|
||||
snprintf(buffer, sizeof(buffer), "Subset %s: %s",
|
||||
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
|
||||
subset->title);
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void server_error(const rc_client_server_error_t *server_error) {
|
||||
@@ -222,7 +234,7 @@ static void server_error(const rc_client_server_error_t *server_error) {
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "RA server error: %s",
|
||||
server_error->error_message);
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static const char *get_leaderboard_title(const char *display) {
|
||||
@@ -315,9 +327,10 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
|
||||
void ra_init_client(bool hardcore, bool challenge, bool leaderboard) {
|
||||
// Create the client instance (using a global variable simplifies this
|
||||
// example)
|
||||
g_client = rc_client_create(read_memory, server_call);
|
||||
g_dmem = NULL;
|
||||
g_dmem_size = 0;
|
||||
if (!g_client)
|
||||
g_client = rc_client_create(read_memory, server_call);
|
||||
g_rdram = NULL;
|
||||
g_rdram_size = 0;
|
||||
|
||||
// Provide a logging function to simplify debugging
|
||||
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
|
||||
@@ -344,6 +357,15 @@ void ra_do_frame() { rc_client_do_frame(g_client); }
|
||||
|
||||
void ra_do_idle() { rc_client_idle(g_client); }
|
||||
|
||||
const char *ra_get_rich_presence() {
|
||||
size_t size = rc_client_get_rich_presence_message(
|
||||
g_client, g_rich_presence_buffer, sizeof(g_rich_presence_buffer));
|
||||
if (size == 0) {
|
||||
return NULL;
|
||||
}
|
||||
return g_rich_presence_buffer;
|
||||
}
|
||||
|
||||
size_t ra_state_size() { return rc_client_progress_size(g_client); }
|
||||
|
||||
void ra_save_state(uint8_t *state, size_t state_size) {
|
||||
@@ -393,7 +415,7 @@ void ra_display_inprogress_achievements(void *userdata) {
|
||||
}
|
||||
}
|
||||
if (buffer_length > 0) {
|
||||
rdp_onscreen_message(buffer, false);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
rc_client_destroy_achievement_list(list);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
|
||||
void ra_shutdown_client();
|
||||
void ra_welcome();
|
||||
void ra_welcome(const char **game_title, const char **game_image_url);
|
||||
bool ra_get_hardcore();
|
||||
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata);
|
||||
void ra_unload_game();
|
||||
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size);
|
||||
void ra_set_rdram(const uint8_t *rdram, size_t rdram_size);
|
||||
void ra_do_frame();
|
||||
void ra_do_idle();
|
||||
void ra_http_callback(const char *content, size_t content_size, int status_code,
|
||||
@@ -18,11 +18,12 @@ void ra_login_user(const char *username, const char *password, void *userdata);
|
||||
void ra_login_token_user(const char *username, const char *token,
|
||||
void *userdata);
|
||||
const char *ra_get_username();
|
||||
const char *ra_get_token();
|
||||
size_t ra_state_size();
|
||||
void ra_save_state(uint8_t *state, size_t state_size);
|
||||
void ra_load_state(const uint8_t *state, size_t state_size);
|
||||
|
||||
const char *ra_get_rich_presence();
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
channel = "1.97.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ pub struct DecodedCheat {
|
||||
data: u16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cheats {
|
||||
pub cheats: Vec<Vec<DecodedCheat>>,
|
||||
pub boot: bool,
|
||||
@@ -26,7 +26,7 @@ pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<S
|
||||
|
||||
pub fn init(
|
||||
device: &mut device::Device,
|
||||
cheat_settings: std::collections::HashMap<String, Option<String>>,
|
||||
cheat_settings: rustc_hash::FxHashMap<String, Option<String>>,
|
||||
) {
|
||||
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
|
||||
.unwrap()
|
||||
@@ -48,7 +48,8 @@ pub fn init(
|
||||
{
|
||||
option_value = Some(found_option_value.to_string());
|
||||
} else {
|
||||
panic!("Cheat option: {option} not found");
|
||||
eprintln!("Cheat option: {option} not found");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,21 +66,27 @@ pub fn init(
|
||||
}
|
||||
let mut split = result.split_whitespace();
|
||||
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
|
||||
decoded_cheat.push(DecodedCheat {
|
||||
code_type: (first_part >> 24) as u8,
|
||||
address: first_part & 0x00FFFFFF,
|
||||
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
|
||||
});
|
||||
if let Ok(data) = u16::from_str_radix(split.next().unwrap(), 16) {
|
||||
decoded_cheat.push(DecodedCheat {
|
||||
code_type: (first_part >> 24) as u8,
|
||||
address: first_part & 0x00FFFFFF,
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
eprintln!("Could not parse data for: {}", cheat_setting.0);
|
||||
};
|
||||
}
|
||||
if !decoded_cheat.is_empty() {
|
||||
device.cheats.cheats.push(decoded_cheat);
|
||||
}
|
||||
device.cheats.cheats.push(decoded_cheat);
|
||||
} else {
|
||||
println!("Could not find cheat: {}", cheat_setting.0);
|
||||
eprintln!("Could not find cheat: {}", cheat_setting.0);
|
||||
}
|
||||
}
|
||||
|
||||
if !device.cheats.cheats.is_empty() {
|
||||
device.cheats.enabled = true;
|
||||
ui::video::onscreen_message("Cheats enabled", false);
|
||||
ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] =
|
||||
["n64", "v64", "z64", "bin", "N64", "V64", "Z64", "BIN"];
|
||||
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
use crate::{cheats, netplay, retroachievements, ui};
|
||||
use std::{collections::HashMap, fs, io::Read};
|
||||
use crate::{cheats, netplay, retroachievements, savestates, ui};
|
||||
use std::io::Read;
|
||||
|
||||
pub mod ai;
|
||||
pub mod cache;
|
||||
@@ -44,29 +47,54 @@ pub mod tlb;
|
||||
pub mod unmapped;
|
||||
pub mod vi;
|
||||
|
||||
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
|
||||
pub fn run_game(
|
||||
device: &mut Device,
|
||||
rom_contents: &[u8],
|
||||
game_settings: ui::GameSettings,
|
||||
ra_config: retroachievements::RAConfig,
|
||||
netplay_config: Option<netplay::NetplayConfig>,
|
||||
) {
|
||||
device.cpu.overclock = game_settings.overclock;
|
||||
if game_settings.disable_expansion_pak {
|
||||
device.rdram.size = 0x400000;
|
||||
}
|
||||
if let Some(slot) = game_settings.load_savestate_slot {
|
||||
device.ui.storage.save_state_slot = slot;
|
||||
device.load_state = true;
|
||||
device.savestate.load_state = true;
|
||||
}
|
||||
|
||||
init_rng(device);
|
||||
|
||||
cart::rom::init(device, rom_contents); // cart needs to come before rdram
|
||||
|
||||
// rdram pointer is shared with parallel-rdp and retroachievements
|
||||
rdram::init(device);
|
||||
|
||||
ui::video::init(device);
|
||||
ui::audio::init(&mut device.ui, device.ai.freq);
|
||||
ui::video::init(device, netplay_config.is_some());
|
||||
ui::audio::init(device);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
// must be after video init
|
||||
retroachievements::welcome();
|
||||
if let Some(netplay_config) = &netplay_config {
|
||||
device.netplay = netplay::init(
|
||||
device,
|
||||
netplay_config,
|
||||
cart::rom::is_system_pal(rom_contents),
|
||||
);
|
||||
if device.netplay.is_none() {
|
||||
ui::input::close(&mut device.ui);
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::video::close(&device.ui);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
init_rng_rtc(device);
|
||||
|
||||
// must be after video init
|
||||
let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() {
|
||||
retroachievements::load_game(rom_contents, rom_contents.len(), ra_config.rich_presence)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
mi::init(device);
|
||||
pif::init(device);
|
||||
@@ -82,6 +110,7 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
|
||||
|
||||
ui::storage::init(&mut device.ui, &device.cart.rom);
|
||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
|
||||
ui::storage::format_saves(device);
|
||||
|
||||
if !retroachievements::get_hardcore() {
|
||||
cheats::init(device, game_settings.cheats);
|
||||
@@ -89,9 +118,14 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
|
||||
|
||||
cpu::run(device);
|
||||
|
||||
retroachievements::unload_game();
|
||||
retroachievements::unload_game(discord_watch_tx, discord_handle);
|
||||
|
||||
ui::storage::write_saves(device);
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::close(netplay);
|
||||
}
|
||||
|
||||
ui::input::close(&mut device.ui);
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::video::close(&device.ui);
|
||||
@@ -101,7 +135,7 @@ fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
|
||||
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
|
||||
}
|
||||
|
||||
fn init_rng(device: &mut Device) {
|
||||
fn init_rng_rtc(device: &mut Device) {
|
||||
let mut rng_seed = set_rng().next_u64();
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
@@ -111,6 +145,18 @@ fn init_rng(device: &mut Device) {
|
||||
}
|
||||
}
|
||||
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
|
||||
|
||||
device.cart.rtc_timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
|
||||
} else {
|
||||
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
@@ -139,24 +185,23 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
pub fn get_rom_contents(file_path: &std::path::PathBuf) -> Option<Vec<u8>> {
|
||||
let mut contents = vec![];
|
||||
if file_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("zip")
|
||||
{
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
#[cfg(target_os = "android")]
|
||||
let zip_file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let zip_file = std::fs::File::open(file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).unwrap();
|
||||
let extension = file
|
||||
.enclosed_name()
|
||||
.unwrap()
|
||||
.extension()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase();
|
||||
if extension == "z64" || extension == "n64" || extension == "v64" {
|
||||
if let Some(extension) = file.enclosed_name().unwrap().extension()
|
||||
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
|
||||
{
|
||||
file.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
break;
|
||||
@@ -167,16 +212,20 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("7z")
|
||||
{
|
||||
#[cfg(target_os = "android")]
|
||||
let sevenz_file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let sevenz_file = std::fs::File::open(file_path).unwrap();
|
||||
let mut archive =
|
||||
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
|
||||
sevenz_rust2::ArchiveReader::new(sevenz_file, sevenz_rust2::Password::empty()).unwrap();
|
||||
|
||||
let mut found = false;
|
||||
archive
|
||||
.for_each_entries(
|
||||
&mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
|
||||
let name = entry.name().to_ascii_lowercase();
|
||||
if !found
|
||||
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
|
||||
&& let Some(extension) = std::path::PathBuf::from(entry.name()).extension()
|
||||
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
|
||||
{
|
||||
reader
|
||||
.read_to_end(&mut contents)
|
||||
@@ -191,8 +240,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
)
|
||||
.expect("ok");
|
||||
} else {
|
||||
contents = fs::read(file_path)
|
||||
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
|
||||
#[cfg(target_os = "android")]
|
||||
let mut file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let mut file = std::fs::File::open(file_path).unwrap();
|
||||
file.read_to_end(&mut contents).unwrap();
|
||||
}
|
||||
|
||||
if contents.is_empty() {
|
||||
@@ -202,15 +254,28 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct SpeedLimiter {
|
||||
pub limit_freq: u64,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "std::time::Instant::now")]
|
||||
pub limit_freq_check: std::time::Instant,
|
||||
pub frame_counter: u64,
|
||||
pub enabled: bool,
|
||||
pub min_wait_time: std::time::Duration,
|
||||
#[serde(skip)]
|
||||
pub next_pace_deadline: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Device {
|
||||
#[serde(skip)]
|
||||
pub netplay: Option<netplay::Netplay>,
|
||||
#[serde(skip, default = "ui::Ui::default")]
|
||||
#[serde(skip)]
|
||||
pub ui: ui::Ui,
|
||||
pub speed_limiter: SpeedLimiter,
|
||||
pub byte_swap: usize,
|
||||
pub save_state: bool,
|
||||
pub load_state: bool,
|
||||
pub savestate: savestates::Savestate,
|
||||
pub cpu: cpu::Cpu,
|
||||
pub pif: pif::Pif,
|
||||
pub cart: cart::Cart,
|
||||
@@ -224,7 +289,6 @@ pub struct Device {
|
||||
pub ai: ai::Ai,
|
||||
pub si: si::Si,
|
||||
pub ri: ri::Ri,
|
||||
#[serde(skip, default = "set_rng")]
|
||||
pub rng: rand::rngs::Xoshiro256PlusPlus,
|
||||
pub vru: controller::vru::Vru,
|
||||
pub transferpaks: [controller::transferpak::TransferPak; 4],
|
||||
@@ -236,7 +300,27 @@ pub fn zero_m128i() -> __m128i {
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn new() -> Device {
|
||||
pub fn clone_state(&mut self, device: &Device) {
|
||||
self.rng.clone_from(&device.rng);
|
||||
self.cpu.clone_from(&device.cpu);
|
||||
self.pif.clone_from(&device.pif);
|
||||
self.cart.clone_from(&device.cart);
|
||||
self.memory.clone_from(&device.memory);
|
||||
self.rsp.clone_from(&device.rsp);
|
||||
self.rdp.clone_from(&device.rdp);
|
||||
self.rdram.clone_from(&device.rdram);
|
||||
self.mi.clone_from(&device.mi);
|
||||
self.pi.clone_from(&device.pi);
|
||||
self.vi.clone_from(&device.vi);
|
||||
self.ai.clone_from(&device.ai);
|
||||
self.si.clone_from(&device.si);
|
||||
self.ri.clone_from(&device.ri);
|
||||
self.vru.clone_from(&device.vru);
|
||||
self.transferpaks.clone_from(&device.transferpaks);
|
||||
self.cheats.clone_from(&device.cheats);
|
||||
}
|
||||
|
||||
pub fn new(with_ui: bool) -> Box<Device> {
|
||||
let mut byte_swap: usize = 0;
|
||||
let test: [u8; 4] = [1, 2, 3, 4];
|
||||
// if the host computer is little endian, that means the RDRAM will be stored as little endian
|
||||
@@ -244,34 +328,41 @@ impl Device {
|
||||
if u32::from_le_bytes(test) == u32::from_ne_bytes(test) {
|
||||
byte_swap = 3;
|
||||
}
|
||||
Device {
|
||||
Box::new(Device {
|
||||
netplay: None,
|
||||
ui: ui::Ui::new(),
|
||||
ui: if with_ui {
|
||||
ui::Ui::new()
|
||||
} else {
|
||||
ui::Ui::default()
|
||||
},
|
||||
speed_limiter: SpeedLimiter {
|
||||
limit_freq: 2,
|
||||
limit_freq_check: std::time::Instant::now(),
|
||||
frame_counter: 0,
|
||||
enabled: true,
|
||||
min_wait_time: std::time::Duration::from_secs(1),
|
||||
next_pace_deadline: None,
|
||||
},
|
||||
byte_swap,
|
||||
save_state: false,
|
||||
load_state: false,
|
||||
savestate: savestates::Savestate {
|
||||
save_state: false,
|
||||
load_state: false,
|
||||
load_rewind: false,
|
||||
last_rewind_saved: 0.0,
|
||||
rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new(
|
||||
std::collections::BTreeMap::new(),
|
||||
)),
|
||||
},
|
||||
cpu: cpu::Cpu {
|
||||
cop0: cop0::Cop0 {
|
||||
regs: [0; cop0::COP0_REGS_COUNT as usize],
|
||||
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
|
||||
regs: [0; cop0::COP0_REGS_COUNT],
|
||||
reg_write_masks: [0; cop0::COP0_REGS_COUNT],
|
||||
reg_latch: 0,
|
||||
is_event: false,
|
||||
instrs: [cop0::reserved; 32],
|
||||
instrs2: [cop0::reserved; 32],
|
||||
tlb_lut_w: vec![
|
||||
tlb::TlbLut {
|
||||
address: 0,
|
||||
cached: false,
|
||||
};
|
||||
0x100000
|
||||
],
|
||||
tlb_lut_r: vec![
|
||||
tlb::TlbLut {
|
||||
address: 0,
|
||||
cached: false,
|
||||
};
|
||||
0x100000
|
||||
],
|
||||
tlb_lut_w: rustc_hash::FxHashMap::default(),
|
||||
tlb_lut_r: rustc_hash::FxHashMap::default(),
|
||||
tlb_entries: [tlb::TlbEntry {
|
||||
mask: 0,
|
||||
vpn2: 0,
|
||||
@@ -357,9 +448,9 @@ impl Device {
|
||||
rtc_timestamp: 0,
|
||||
rtc: cart::AfRtc { control: 0x0200 },
|
||||
sc64: cart::sc64::Sc64 {
|
||||
regs: [0; cart::sc64::SC64_REGS_COUNT as usize],
|
||||
regs: [0; cart::sc64::SC64_REGS_COUNT],
|
||||
regs_locked: true,
|
||||
cfg: [0; cart::sc64::SC64_CFG_COUNT as usize],
|
||||
cfg: [0; cart::sc64::SC64_CFG_COUNT],
|
||||
sector: 0,
|
||||
buffer: vec![0; 8192],
|
||||
writeback_sector: vec![0; 256],
|
||||
@@ -395,7 +486,7 @@ impl Device {
|
||||
rdram: rdram::Rdram {
|
||||
mem: vec![],
|
||||
size: 0x800000,
|
||||
regs: [[0; rdram::RDRAM_REGS_COUNT as usize]; 4],
|
||||
regs: [[0; rdram::RDRAM_REGS_COUNT]; 4],
|
||||
},
|
||||
rsp: rsp_interface::Rsp {
|
||||
cpu: rsp_cpu::Cpu {
|
||||
@@ -440,8 +531,8 @@ impl Device {
|
||||
instrs: [rsp_su_instructions::reserved; 64],
|
||||
vec_instrs: [rsp_vu_instructions::reserved; 64],
|
||||
},
|
||||
regs: [0; rsp_interface::SP_REGS_COUNT as usize],
|
||||
regs2: [0; rsp_interface::SP_REGS2_COUNT as usize],
|
||||
regs: [0; rsp_interface::SP_REGS_COUNT],
|
||||
regs2: [0; rsp_interface::SP_REGS2_COUNT],
|
||||
mem: [0; 0x2000],
|
||||
last_status_value: 0,
|
||||
run_after_dma: false,
|
||||
@@ -453,19 +544,19 @@ impl Device {
|
||||
}; 2],
|
||||
},
|
||||
rdp: rdp::Rdp {
|
||||
regs_dpc: [0; rdp::DPC_REGS_COUNT as usize],
|
||||
regs_dps: [0; rdp::DPS_REGS_COUNT as usize],
|
||||
regs_dpc: [0; rdp::DPC_REGS_COUNT],
|
||||
regs_dps: [0; rdp::DPS_REGS_COUNT],
|
||||
wait_frozen: false,
|
||||
last_status_value: 0,
|
||||
},
|
||||
mi: mi::Mi {
|
||||
regs: [0; mi::MI_REGS_COUNT as usize],
|
||||
regs: [0; mi::MI_REGS_COUNT],
|
||||
},
|
||||
pi: pi::Pi {
|
||||
regs: [0; pi::PI_REGS_COUNT as usize],
|
||||
regs: [0; pi::PI_REGS_COUNT],
|
||||
},
|
||||
ai: ai::Ai {
|
||||
regs: [0; ai::AI_REGS_COUNT as usize],
|
||||
regs: [0; ai::AI_REGS_COUNT],
|
||||
last_read: 0,
|
||||
freq: 33600,
|
||||
delayed_carry: false,
|
||||
@@ -476,26 +567,21 @@ impl Device {
|
||||
}; 2],
|
||||
},
|
||||
si: si::Si {
|
||||
regs: [0; si::SI_REGS_COUNT as usize],
|
||||
regs: [0; si::SI_REGS_COUNT],
|
||||
dma_dir: si::DmaDir::None,
|
||||
},
|
||||
ri: ri::Ri {
|
||||
regs: [0; ri::RI_REGS_COUNT as usize],
|
||||
regs: [0; ri::RI_REGS_COUNT],
|
||||
ram_init: false,
|
||||
},
|
||||
vi: vi::Vi {
|
||||
regs: [0; vi::VI_REGS_COUNT as usize],
|
||||
regs: [0; vi::VI_REGS_COUNT],
|
||||
clock: 0,
|
||||
field: 0,
|
||||
delay: 0,
|
||||
count_per_scanline: 0,
|
||||
enable_speed_limiter: true,
|
||||
next_pace_deadline: None,
|
||||
vi_counter: 0,
|
||||
min_wait_time: std::time::Duration::from_secs(1),
|
||||
frame_time: 0.0,
|
||||
limit_freq: 2,
|
||||
limit_freq_check: std::time::Instant::now(),
|
||||
elapsed_time: 0.0,
|
||||
},
|
||||
vru: controller::vru::Vru {
|
||||
status: 0,
|
||||
@@ -505,7 +591,7 @@ impl Device {
|
||||
word_buffer: [0; 40],
|
||||
words: Vec::new(),
|
||||
talking: false,
|
||||
word_mappings: HashMap::new(),
|
||||
word_mappings: rustc_hash::FxHashMap::default(),
|
||||
},
|
||||
rng: set_rng(),
|
||||
transferpaks: [
|
||||
@@ -519,6 +605,6 @@ impl Device {
|
||||
boot: true,
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::{device, ui};
|
||||
|
||||
const AI_DRAM_ADDR_REG: u32 = 0;
|
||||
const AI_LEN_REG: u32 = 1;
|
||||
// const AI_CONTROL_REG: u32 = 2;
|
||||
const AI_STATUS_REG: u32 = 3;
|
||||
const AI_DACRATE_REG: u32 = 4;
|
||||
// const AI_BITRATE_REG: u32 = 5;
|
||||
pub const AI_REGS_COUNT: u32 = 6;
|
||||
const AI_DRAM_ADDR_REG: usize = 0;
|
||||
const AI_LEN_REG: usize = 1;
|
||||
// const AI_CONTROL_REG: usize = 2;
|
||||
pub const AI_STATUS_REG: usize = 3;
|
||||
const AI_DACRATE_REG: usize = 4;
|
||||
// const AI_BITRATE_REG: usize = 5;
|
||||
pub const AI_REGS_COUNT: usize = 6;
|
||||
|
||||
const AI_STATUS_BUSY: u32 = 0x40000000;
|
||||
pub const AI_STATUS_BUSY: u32 = 0x40000000;
|
||||
const AI_STATUS_FULL: u32 = 0x80000000;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ai {
|
||||
pub regs: [u32; AI_REGS_COUNT as usize],
|
||||
pub regs: [u32; AI_REGS_COUNT],
|
||||
pub fifo: [AiDma; 2],
|
||||
pub last_read: u64,
|
||||
pub delayed_carry: bool,
|
||||
@@ -38,7 +38,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
}
|
||||
|
||||
let remaining_dma_duration =
|
||||
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize];
|
||||
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG];
|
||||
|
||||
let dma_length = remaining_dma_duration * device.ai.fifo[0].length / device.ai.fifo[0].duration;
|
||||
dma_length & !7
|
||||
@@ -46,7 +46,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
|
||||
fn get_dma_duration(device: &device::Device) -> u64 {
|
||||
let bytes_per_sample = 4; /* XXX: assume 16bit stereo - should depends on bitrate instead */
|
||||
let length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
let length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
|
||||
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
|
||||
}
|
||||
@@ -73,34 +73,36 @@ fn do_dma(device: &mut device::Device) {
|
||||
fn fifo_push(device: &mut device::Device) {
|
||||
let duration = get_dma_duration(device);
|
||||
|
||||
if (device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_BUSY) != 0 {
|
||||
if (device.ai.regs[AI_STATUS_REG] & AI_STATUS_BUSY) != 0 {
|
||||
device.ai.fifo[1].address =
|
||||
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
device.ai.fifo[1].duration = duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_FULL;
|
||||
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_FULL;
|
||||
} else {
|
||||
device.ai.fifo[0].address =
|
||||
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
device.ai.fifo[0].duration = duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_BUSY;
|
||||
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_BUSY;
|
||||
|
||||
do_dma(device);
|
||||
ui::audio::resume_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn fifo_pop(device: &mut device::Device) {
|
||||
if device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_FULL != 0 {
|
||||
if device.ai.regs[AI_STATUS_REG] & AI_STATUS_FULL != 0 {
|
||||
device.ai.fifo[0].address = device.ai.fifo[1].address;
|
||||
device.ai.fifo[0].length = device.ai.fifo[1].length;
|
||||
device.ai.fifo[0].duration = device.ai.fifo[1].duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_FULL;
|
||||
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_FULL;
|
||||
|
||||
do_dma(device);
|
||||
} else {
|
||||
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_BUSY;
|
||||
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_BUSY;
|
||||
device.ai.delayed_carry = false;
|
||||
ui::audio::pause_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +112,7 @@ pub fn read_regs(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
let return_value = match reg as u32 {
|
||||
let return_value = match reg as usize {
|
||||
AI_LEN_REG => {
|
||||
let value = get_remaining_dma_length(device);
|
||||
if value < device.ai.last_read {
|
||||
@@ -134,7 +136,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
AI_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask);
|
||||
if device.ai.regs[reg as usize] != 0 {
|
||||
@@ -145,8 +147,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
AI_DACRATE_REG => {
|
||||
if device.ai.regs[reg as usize] != value & mask {
|
||||
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
|
||||
ui::audio::close_game_audio(&mut device.ui);
|
||||
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
|
||||
ui::audio::update_freq(device);
|
||||
}
|
||||
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const JDT_EEPROM_16K: u16 = 0xc000; /* 16k EEPROM */
|
||||
const EEPROM_BLOCK_SIZE: usize = 8;
|
||||
pub const EEPROM_MAX_SIZE: usize = 0x800;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AfRtc {
|
||||
pub control: u16,
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fn byte2bcd(mut n: u32) -> u8 {
|
||||
(((n / 10) << 4) | (n % 10)) as u8
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cart {
|
||||
#[serde(skip)]
|
||||
pub rom: Vec<u8>,
|
||||
@@ -130,8 +130,6 @@ pub fn format_eeprom(device: &mut device::Device) {
|
||||
fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
|
||||
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
|
||||
|
||||
format_eeprom(device);
|
||||
|
||||
device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE].copy_from_slice(
|
||||
&device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE],
|
||||
);
|
||||
@@ -140,8 +138,6 @@ fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
|
||||
fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize, status: usize) {
|
||||
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
|
||||
|
||||
format_eeprom(device);
|
||||
|
||||
device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE]
|
||||
.copy_from_slice(&device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE]);
|
||||
|
||||
@@ -151,9 +147,10 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
|
||||
}
|
||||
|
||||
fn time2data(device: &mut device::Device, offset: usize) {
|
||||
let timestamp =
|
||||
device.cart.rtc_timestamp + (device.vi.frame_time * device.vi.vi_counter as f64) as i64;
|
||||
let now = chrono::DateTime::from_timestamp(timestamp, 0).unwrap();
|
||||
let timestamp = device.cart.rtc_timestamp + device.vi.elapsed_time as i64;
|
||||
let now = chrono::DateTime::from_timestamp(timestamp, 0)
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Local);
|
||||
|
||||
device.pif.ram[offset] = byte2bcd(now.second());
|
||||
device.pif.ram[offset + 1] = byte2bcd(now.minute());
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub const CART_MASK: usize = 0xFFFFFFF;
|
||||
|
||||
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
|
||||
let mut data: [u8; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
if let Some(value) = device
|
||||
.ui
|
||||
.storage
|
||||
.saves
|
||||
.romsave
|
||||
.data
|
||||
.get(&(address as u32 + i))
|
||||
{
|
||||
data[i as usize] = *value;
|
||||
} else {
|
||||
data[i as usize] = *device.cart.rom.get(address + i as usize).unwrap_or(&0);
|
||||
}
|
||||
}
|
||||
u32::from_be_bytes(data)
|
||||
let romsave = &device.ui.storage.saves.romsave.data;
|
||||
let rom = &device.cart.rom;
|
||||
u32::from_be_bytes(std::array::from_fn(|i| {
|
||||
romsave
|
||||
.get(&((address + i) as u32))
|
||||
.copied()
|
||||
.unwrap_or_else(|| *rom.get(address + i).unwrap_or(&0))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
@@ -41,8 +32,8 @@ pub fn read_mem(
|
||||
let cycles = device::pi::calculate_cycles(device, 1, 4);
|
||||
device::cop0::add_cycles(device, cycles);
|
||||
|
||||
// well known cart ROM oddity, if a read is perfomed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
|
||||
if device.pi.regs[device::pi::PI_STATUS_REG as usize] & device::pi::PI_STATUS_IO_BUSY != 0 {
|
||||
// well known cart ROM oddity, if a read is performed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
|
||||
if device.pi.regs[device::pi::PI_STATUS_REG] & device::pi::PI_STATUS_IO_BUSY != 0 {
|
||||
device.cart.latch
|
||||
} else {
|
||||
let masked_address = address as usize & CART_MASK;
|
||||
@@ -69,7 +60,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
|
||||
device.cart.latch = value & mask;
|
||||
|
||||
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
|
||||
let cycles = device::pi::calculate_cycles(device, 1, 4);
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
@@ -113,19 +104,15 @@ pub fn dma_write(
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
while i < dram_addr + length {
|
||||
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = *value;
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
|
||||
*value
|
||||
} else {
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
|
||||
}
|
||||
*device.cart.rom.get(j as usize).unwrap_or(&0)
|
||||
};
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
@@ -134,20 +121,10 @@ pub fn dma_write(
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
|
||||
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
|
||||
} else {
|
||||
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
|
||||
}
|
||||
}
|
||||
|
||||
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
|
||||
|
||||
device.cart.rom = rom_file.to_vec();
|
||||
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
|
||||
device.cart.pal = is_system_pal(&device.cart.rom);
|
||||
set_cic(device);
|
||||
|
||||
device.ui.game_hash = calculate_hash(&device.cart.rom);
|
||||
@@ -158,10 +135,10 @@ pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_system_pal(country: u8) -> bool {
|
||||
let pal_codes: [u8; 8] = [b'D', b'F', b'I', b'P', b'S', b'U', b'X', b'Y'];
|
||||
pub fn is_system_pal(rom_contents: &[u8]) -> bool {
|
||||
let pal_codes: [u8; 8] = *b"DFIPSUXY";
|
||||
for i in pal_codes {
|
||||
if country == i {
|
||||
if rom_contents[0x3E] == i {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,29 @@ use crate::ui;
|
||||
|
||||
pub const SDCARD_SIZE: usize = 0x4000000;
|
||||
|
||||
const SC64_SCR_REG: u32 = 0;
|
||||
const SC64_DATA0_REG: u32 = 1;
|
||||
const SC64_DATA1_REG: u32 = 2;
|
||||
const SC64_IDENTIFIER_REG: u32 = 3;
|
||||
const SC64_KEY_REG: u32 = 4;
|
||||
//const SC64_IRQ_REG: u32 = 5;
|
||||
//const SC64_AUX_REG: u32 = 6;
|
||||
pub const SC64_REGS_COUNT: u32 = 7;
|
||||
const SC64_SCR_REG: usize = 0;
|
||||
const SC64_DATA0_REG: usize = 1;
|
||||
const SC64_DATA1_REG: usize = 2;
|
||||
const SC64_IDENTIFIER_REG: usize = 3;
|
||||
const SC64_KEY_REG: usize = 4;
|
||||
//const SC64_IRQ_REG: usize = 5;
|
||||
//const SC64_AUX_REG: usize = 6;
|
||||
pub const SC64_REGS_COUNT: usize = 7;
|
||||
|
||||
pub const SC64_BOOTLOADER_SWITCH: u32 = 0;
|
||||
pub const SC64_ROM_WRITE_ENABLE: u32 = 1;
|
||||
pub const SC64_SAVE_TYPE: u32 = 6;
|
||||
pub const SC64_CFG_COUNT: u32 = 15;
|
||||
pub const SC64_CFG_COUNT: usize = 15;
|
||||
|
||||
const SC64_BUFFER_MASK: usize = 0x1FFF;
|
||||
const SC64_EEPROM_MASK: usize = 0xFFF;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Sc64 {
|
||||
pub buffer: Vec<u8>,
|
||||
pub regs: [u32; SC64_REGS_COUNT as usize],
|
||||
pub regs: [u32; SC64_REGS_COUNT],
|
||||
pub regs_locked: bool,
|
||||
pub cfg: [u32; SC64_CFG_COUNT as usize],
|
||||
pub cfg: [u32; SC64_CFG_COUNT],
|
||||
pub sector: u32,
|
||||
pub writeback_sector: Vec<u32>,
|
||||
pub usb_buffer: Vec<u8>,
|
||||
@@ -49,7 +49,7 @@ pub fn read_regs(
|
||||
return 0;
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SC64_SCR_REG | SC64_DATA0_REG | SC64_DATA1_REG => device.cart.sc64.regs[reg as usize],
|
||||
SC64_IDENTIFIER_REG => 0x53437632,
|
||||
_ => panic!("unknown read reg {reg} address {address:#x}"),
|
||||
@@ -58,12 +58,12 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SC64_KEY_REG => {
|
||||
device::memory::masked_write_32(&mut device.cart.sc64.regs[reg as usize], value, mask);
|
||||
if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0x4F434B5F {
|
||||
if device.cart.sc64.regs[SC64_KEY_REG] == 0x4F434B5F {
|
||||
device.cart.sc64.regs_locked = false;
|
||||
} else if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0xFFFFFFFF {
|
||||
} else if device.cart.sc64.regs[SC64_KEY_REG] == 0xFFFFFFFF {
|
||||
device.cart.sc64.regs_locked = true;
|
||||
}
|
||||
}
|
||||
@@ -81,22 +81,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
match char::from_u32(value & mask).unwrap() {
|
||||
'V' => {
|
||||
// get version
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = (2 << 16) | 20;
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 2;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = (2 << 16) | 20;
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 2;
|
||||
}
|
||||
'c' => {
|
||||
// get config
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = device.cart.sc64.cfg
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] =
|
||||
device.cart.sc64.cfg[device.cart.sc64.regs[SC64_DATA0_REG] as usize]
|
||||
}
|
||||
'C' => {
|
||||
// set config
|
||||
if device.cart.sc64.regs[SC64_DATA0_REG as usize] == SC64_SAVE_TYPE {
|
||||
if device.cart.sc64.regs[SC64_DATA0_REG] == SC64_SAVE_TYPE {
|
||||
// if save type is being written, we are probably booting a game using the flash cart menu
|
||||
// we shouldn't write saves to disk in this case (they are written to the SD card)
|
||||
device.ui.storage.saves.write_to_disk = false;
|
||||
device.ui.storage.save_type =
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG] {
|
||||
0 => {
|
||||
vec![]
|
||||
}
|
||||
@@ -115,20 +115,20 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
_ => {
|
||||
panic!(
|
||||
"unknown sc64 save type: {}",
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::swap(
|
||||
&mut device.cart.sc64.cfg
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize],
|
||||
&mut device.cart.sc64.regs[SC64_DATA1_REG as usize],
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG] as usize],
|
||||
&mut device.cart.sc64.regs[SC64_DATA1_REG],
|
||||
);
|
||||
}
|
||||
'i' => {
|
||||
// sd card operation
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG] {
|
||||
0 => { //Init SD card
|
||||
}
|
||||
1 => { //Deinit SD card
|
||||
@@ -136,23 +136,21 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
_ => {
|
||||
panic!(
|
||||
"unknown sc64 sd card operation: {}",
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
'I' => {
|
||||
// set sd sector
|
||||
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG as usize];
|
||||
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG];
|
||||
}
|
||||
's' => {
|
||||
format_sdcard(device);
|
||||
// read sd card
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let offset = (device.cart.sc64.sector * 512) as usize;
|
||||
let length =
|
||||
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
|
||||
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
|
||||
let mut i = 0;
|
||||
|
||||
while i < length {
|
||||
@@ -182,11 +180,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
'S' => {
|
||||
format_sdcard(device);
|
||||
// write sd card
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let offset = (device.cart.sc64.sector * 512) as usize;
|
||||
let length =
|
||||
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
|
||||
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
|
||||
let mut i = 0;
|
||||
|
||||
while i < length {
|
||||
@@ -211,42 +207,32 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
|
||||
}
|
||||
'U' => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
|
||||
}
|
||||
'u' => {
|
||||
// used to notify the game that there is data to read
|
||||
if let Some(cart_rx) = device.ui.usb.cart_rx.as_mut() {
|
||||
match cart_rx.try_recv() {
|
||||
Ok(data) => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] =
|
||||
data.data_type & 0xFF; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = data.data_type & 0xFF; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] =
|
||||
data.data_size & 0xFFFFFF; // length
|
||||
device.cart.sc64.usb_buffer = data.data; // store the data to be read
|
||||
}
|
||||
Err(err) => {
|
||||
match err {
|
||||
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
|
||||
panic!("cart_rx lagged: {err}");
|
||||
}
|
||||
_ => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
'M' => {
|
||||
// Send data from from flashcart to USB
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length =
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize & 0xFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize & 0xFFFFFF;
|
||||
|
||||
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
|
||||
let mut usb_buffer = vec![0; length];
|
||||
@@ -297,18 +283,16 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
usb_tx,
|
||||
ui::usb::UsbData {
|
||||
data: usb_buffer,
|
||||
data_type: device.cart.sc64.regs[SC64_DATA1_REG as usize] >> 24,
|
||||
data_size: device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
& 0xFFFFFF,
|
||||
data_type: device.cart.sc64.regs[SC64_DATA1_REG] >> 24,
|
||||
data_size: device.cart.sc64.regs[SC64_DATA1_REG] & 0xFFFFFF,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
'm' => {
|
||||
// Receive data from USB to flashcart
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize;
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
@@ -338,11 +322,11 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
}
|
||||
'w' => {
|
||||
// SD card writeback pending
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
|
||||
}
|
||||
'W' => {
|
||||
let writeback_sectors_address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] as u64;
|
||||
for i in 0..256 {
|
||||
device.cart.sc64.writeback_sector[i] = device::memory::data_read(
|
||||
device,
|
||||
@@ -376,7 +360,6 @@ pub fn read_mem(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
if address & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
let masked_address = address as usize & SC64_EEPROM_MASK;
|
||||
u32::from_be_bytes(
|
||||
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
|
||||
@@ -395,7 +378,6 @@ pub fn read_mem(
|
||||
|
||||
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
if address & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
let masked_address = address as usize & SC64_EEPROM_MASK;
|
||||
let mut data = u32::from_be_bytes(
|
||||
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
|
||||
@@ -427,7 +409,6 @@ pub fn dma_read(
|
||||
) -> u64 {
|
||||
dram_addr &= device::rdram::RDRAM_MASK as u32;
|
||||
let buffer = if cart_addr & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
cart_addr &= SC64_EEPROM_MASK as u32;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
|
||||
&mut device.ui.storage.saves.eeprom.data
|
||||
@@ -459,7 +440,6 @@ pub fn dma_write(
|
||||
) -> u64 {
|
||||
dram_addr &= device::rdram::RDRAM_MASK as u32;
|
||||
let buffer = if cart_addr & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
cart_addr &= SC64_EEPROM_MASK as u32;
|
||||
&device.ui.storage.saves.eeprom.data
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ pub const MX29L1100_ID: u32 = 0x00c2001e;
|
||||
const MX29L0000_ID: u32 = 0x00c20000;
|
||||
const MX29L0001_ID: u32 = 0x00c20001;
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FlashramMode {
|
||||
ReadArray,
|
||||
ReadSiliconId,
|
||||
@@ -18,7 +18,7 @@ pub enum FlashramMode {
|
||||
PageProgram,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Flashram {
|
||||
pub status: u32,
|
||||
pub mode: FlashramMode,
|
||||
@@ -28,13 +28,13 @@ pub struct Flashram {
|
||||
pub silicon_id: [u32; 2],
|
||||
}
|
||||
|
||||
fn format_sram(device: &mut device::Device) {
|
||||
pub fn format_sram(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.sram.data.len() < SRAM_SIZE {
|
||||
device.ui.storage.saves.sram.data.resize(SRAM_SIZE, 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_flash(device: &mut device::Device) {
|
||||
pub fn format_flash(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.flash.data.len() < FLASHRAM_SIZE {
|
||||
device
|
||||
.ui
|
||||
@@ -49,8 +49,6 @@ fn format_flash(device: &mut device::Device) {
|
||||
fn read_mem_sram(device: &mut device::Device, address: u64) -> u32 {
|
||||
let masked_address = address as usize & SRAM_MASK;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
u32::from_be_bytes(
|
||||
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
|
||||
.try_into()
|
||||
@@ -96,8 +94,6 @@ pub fn read_mem(
|
||||
fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let masked_address = address as usize & SRAM_MASK;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
let mut data = u32::from_be_bytes(
|
||||
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
|
||||
.try_into()
|
||||
@@ -116,7 +112,6 @@ fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask:
|
||||
device.cart.flashram.status = (value & mask) & 0xff;
|
||||
} else if (address & 0x1ffff) == 0x10000 {
|
||||
/* set command */
|
||||
format_flash(device);
|
||||
flashram_command(device, value & mask);
|
||||
} else {
|
||||
/* other accesses are not implemented */
|
||||
@@ -136,7 +131,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
write_mem_flash(device, address, value, mask)
|
||||
}
|
||||
|
||||
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
|
||||
let cycles = device::pi::calculate_cycles(device, 2, 4);
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
@@ -148,8 +143,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
while i < dram_addr + length {
|
||||
*device
|
||||
.ui
|
||||
@@ -171,8 +164,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
|
||||
}
|
||||
|
||||
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
|
||||
format_flash(device);
|
||||
|
||||
if (cart_addr & 0x1ffff) == 0x00000
|
||||
&& length == 128
|
||||
&& device.cart.flashram.mode == FlashramMode::PageProgram
|
||||
@@ -217,8 +208,6 @@ fn dma_write_sram(
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
while i < dram_addr + length {
|
||||
*device
|
||||
.rdram
|
||||
@@ -266,7 +255,6 @@ fn dma_write_flash(
|
||||
} else if (cart_addr & 0x1ffff) < 0x10000
|
||||
&& device.cart.flashram.mode == FlashramMode::ReadArray
|
||||
{
|
||||
format_flash(device);
|
||||
/* adjust flashram address before starting DMA. */
|
||||
if device.cart.flashram.silicon_id[1] == MX29L1100_ID
|
||||
|| device.cart.flashram.silicon_id[1] == MX29L0000_ID
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{device, netplay, savestates, ui};
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
pub mod gbcart;
|
||||
pub mod mempak;
|
||||
@@ -57,13 +57,9 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
JCMD_CONTROLLER_READ => {
|
||||
let offset = device.pif.channels[channel].rx_buf.unwrap();
|
||||
let input = if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number as usize == channel {
|
||||
let local_input = ui::input::get(&device.ui, 0);
|
||||
netplay::send_input(netplay, local_input);
|
||||
}
|
||||
netplay::get_input(device, channel)
|
||||
netplay.inputs[channel].0
|
||||
} else {
|
||||
ui::input::get(&device.ui, channel)
|
||||
ui::input::get(&mut device.ui, channel)
|
||||
};
|
||||
|
||||
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
|
||||
@@ -76,7 +72,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_PAK,
|
||||
device.cpu.clock_rate, // 1 second
|
||||
device.cpu.clock_rate / 2, // 500ms
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,7 +91,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device.pif.channels[channel].rx_buf.unwrap(),
|
||||
channel,
|
||||
),
|
||||
_ => println!("unknown controller command {cmd}"),
|
||||
_ => eprintln!("unknown controller command {cmd}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +158,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
if channel.change_pak != PakType::None {
|
||||
//stop rumble if it is on
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == i {
|
||||
if netplay.player_number == i {
|
||||
device::ui::input::set_rumble(&device.ui, 0, 0);
|
||||
}
|
||||
} else {
|
||||
@@ -190,21 +186,30 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message("MemPak selected", false);
|
||||
ui::video::onscreen_message(
|
||||
&format!("P{} MemPak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::RumblePak {
|
||||
channel.pak_handler = Some(device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message("RumblePak selected", false);
|
||||
ui::video::onscreen_message(
|
||||
&format!("P{} RumblePak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::TransferPak {
|
||||
channel.pak_handler = Some(device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message("TransferPak selected", false);
|
||||
ui::video::onscreen_message(
|
||||
&format!("P{} TransferPak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
channel.change_pak = PakType::None;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::device;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
use std::io::Write;
|
||||
const MBC3_RTC_SECONDS: usize = 0;
|
||||
const MBC3_RTC_MINUTES: usize = 1;
|
||||
const MBC3_RTC_HOURS: usize = 2;
|
||||
const MBC3_RTC_DAYS_L: usize = 3;
|
||||
const MBC3_RTC_DAYS_H: usize = 4;
|
||||
const MBC3_RTC_REGS_COUNT: usize = 5;
|
||||
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GbCart {
|
||||
pub enabled: bool,
|
||||
#[serde(skip)]
|
||||
@@ -11,14 +19,12 @@ pub struct GbCart {
|
||||
pub mbc1_mode: bool,
|
||||
pub ram_bank: u16,
|
||||
pub rom_bank: u32,
|
||||
pub set_latch: bool,
|
||||
pub latch_second: u8,
|
||||
pub latch_minute: u8,
|
||||
pub latch_hour: u8,
|
||||
pub latch_day: u16,
|
||||
pub rtc_regs: [u8; MBC3_RTC_REGS_COUNT],
|
||||
pub rtc_regs_latch: [u8; MBC3_RTC_REGS_COUNT],
|
||||
pub last_elapsed_time: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CartType {
|
||||
#[default]
|
||||
None,
|
||||
@@ -28,6 +34,135 @@ pub enum CartType {
|
||||
MBC5RamBatt,
|
||||
}
|
||||
|
||||
pub fn init(gb_cart: &mut device::controller::gbcart::GbCart, rom: &[u8], ram: &[u8]) {
|
||||
gb_cart.rom = rom.to_vec();
|
||||
let offset = match ram.len() % 8192 {
|
||||
44 => {
|
||||
gb_cart.ram = ram[..ram.len() - 44].to_vec();
|
||||
ram.len() - 44
|
||||
}
|
||||
48 => {
|
||||
gb_cart.ram = ram[..ram.len() - 48].to_vec();
|
||||
ram.len() - 48
|
||||
}
|
||||
_ => {
|
||||
gb_cart.ram = ram.to_vec();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
gb_cart.rtc_regs[MBC3_RTC_SECONDS] =
|
||||
u32::from_le_bytes(ram[offset..offset + 4].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_MINUTES] =
|
||||
u32::from_le_bytes(ram[offset + 4..offset + 8].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_HOURS] =
|
||||
u32::from_le_bytes(ram[offset + 8..offset + 12].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_DAYS_L] =
|
||||
u32::from_le_bytes(ram[offset + 12..offset + 16].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_DAYS_H] =
|
||||
u32::from_le_bytes(ram[offset + 16..offset + 20].try_into().unwrap()) as u8;
|
||||
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] =
|
||||
u32::from_le_bytes(ram[offset + 20..offset + 24].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] =
|
||||
u32::from_le_bytes(ram[offset + 24..offset + 28].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] =
|
||||
u32::from_le_bytes(ram[offset + 28..offset + 32].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] =
|
||||
u32::from_le_bytes(ram[offset + 32..offset + 36].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] =
|
||||
u32::from_le_bytes(ram[offset + 36..offset + 40].try_into().unwrap()) as u8;
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
gb_cart: &mut device::controller::gbcart::GbCart,
|
||||
elapsed_time: i64,
|
||||
ram_path: &std::string::String,
|
||||
) {
|
||||
if cfg!(target_os = "android") {
|
||||
return;
|
||||
}
|
||||
update_rtc_regs(gb_cart, elapsed_time);
|
||||
|
||||
if let Ok(file) = std::fs::File::create(ram_path) {
|
||||
let mut f = std::io::BufWriter::new(file);
|
||||
f.write_all(&gb_cart.ram).unwrap();
|
||||
|
||||
if gb_cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_SECONDS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_MINUTES] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_HOURS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
|
||||
let max = ((i32::MAX as i64) << 32) | (i32::MAX as i64);
|
||||
f.write_all(&max.to_le_bytes()).unwrap();
|
||||
|
||||
f.flush().unwrap();
|
||||
}
|
||||
} else {
|
||||
eprintln!("Error saving TransferPak RAM to {ram_path}");
|
||||
}
|
||||
}
|
||||
|
||||
fn update_rtc_regs(cart: &mut device::controller::gbcart::GbCart, elapsed_time: i64) {
|
||||
let mut diff = elapsed_time - cart.last_elapsed_time;
|
||||
cart.last_elapsed_time = elapsed_time;
|
||||
|
||||
if diff > 0 {
|
||||
cart.rtc_regs[MBC3_RTC_SECONDS] += (diff % 60) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_SECONDS] >= 60 {
|
||||
cart.rtc_regs[MBC3_RTC_SECONDS] -= 60;
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] += 1;
|
||||
}
|
||||
diff /= 60;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] += (diff % 60) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_MINUTES] >= 60 {
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] -= 60;
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] += 1;
|
||||
}
|
||||
diff /= 60;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] += (diff % 24) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_HOURS] >= 24 {
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] -= 24;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_L] += 1;
|
||||
}
|
||||
diff /= 24;
|
||||
|
||||
/* update days counter */
|
||||
let days = ((((cart.rtc_regs[MBC3_RTC_DAYS_H] & 0x01) as i64) << 8)
|
||||
| cart.rtc_regs[MBC3_RTC_DAYS_L] as i64)
|
||||
+ diff;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_L] = (days & 0xff) as u8;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] &= !0x01;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] |= ((days >> 8) & 1) as u8;
|
||||
|
||||
/* set carry bit if days overflow */
|
||||
if days > 511 {
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] |= 0x80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mbc1(
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
@@ -105,6 +240,7 @@ fn write_mbc3(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
let value = pif_ram[data + size - 1];
|
||||
if address < 0x2000 {
|
||||
@@ -118,10 +254,10 @@ fn write_mbc3(
|
||||
} else if address < 0x6000 {
|
||||
cart.ram_bank = (value & 0xf) as u16;
|
||||
} else if address < 0x8000 {
|
||||
if !cart.set_latch && value != 0 {
|
||||
// not implemented
|
||||
if cart.cart_type == CartType::MBC3RamBattRtc && (value & 0x1) != 0 {
|
||||
update_rtc_regs(cart, elapsed_time);
|
||||
cart.rtc_regs_latch = cart.rtc_regs;
|
||||
}
|
||||
cart.set_latch = value != 0;
|
||||
} else if (0xa000..0xc000).contains(&address) {
|
||||
if !cart.ram_enabled {
|
||||
return;
|
||||
@@ -130,29 +266,8 @@ fn write_mbc3(
|
||||
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
|
||||
cart.ram[banked_address as usize..banked_address as usize + size]
|
||||
.copy_from_slice(&pif_ram[data..data + size]);
|
||||
} else {
|
||||
match cart.ram_bank {
|
||||
0x8 => {
|
||||
cart.latch_second = value;
|
||||
}
|
||||
0x9 => {
|
||||
cart.latch_minute = value;
|
||||
}
|
||||
0xA => {
|
||||
cart.latch_hour = value;
|
||||
}
|
||||
0xB => {
|
||||
cart.latch_day &= 0xFF00;
|
||||
cart.latch_day |= value as u16;
|
||||
}
|
||||
0xC => {
|
||||
cart.latch_day &= 0x00FF;
|
||||
cart.latch_day |= (value as u16) << 8;
|
||||
}
|
||||
_ => {
|
||||
panic!("Unsupported ram bank {:x}", cart.ram_bank);
|
||||
}
|
||||
}
|
||||
} else if cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
cart.rtc_regs[cart.ram_bank as usize - 0x8] = value;
|
||||
}
|
||||
} else {
|
||||
panic!("Unsupported write address {address:x}");
|
||||
@@ -161,7 +276,7 @@ fn write_mbc3(
|
||||
|
||||
fn read_mbc3(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -188,19 +303,9 @@ fn read_mbc3(
|
||||
.get(banked_address as usize..banked_address as usize + size)
|
||||
.unwrap_or(&vec![0; size]),
|
||||
);
|
||||
} else {
|
||||
let latch = match cart.ram_bank {
|
||||
0x8 => cart.latch_second,
|
||||
0x9 => cart.latch_minute,
|
||||
0xA => cart.latch_hour,
|
||||
0xB => cart.latch_day as u8,
|
||||
0xC => (cart.latch_day >> 8) as u8,
|
||||
_ => {
|
||||
panic!("Unsupported ram bank {:x}", cart.ram_bank);
|
||||
}
|
||||
};
|
||||
} else if cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
for i in 0..size {
|
||||
pif_ram[data + i] = latch;
|
||||
pif_ram[data + i] = cart.rtc_regs_latch[cart.ram_bank as usize - 0x8];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -227,7 +332,7 @@ fn write_mbc5(
|
||||
} else if address < 0x6000 {
|
||||
cart.ram_bank = (value & 0xf) as u16;
|
||||
} else if address < 0xa000 {
|
||||
println!("Unknown MBC5 write address {address:x}");
|
||||
eprintln!("Unknown MBC5 write address {address:x}");
|
||||
} else if (0xa000..0xc000).contains(&address) {
|
||||
if !cart.ram_enabled {
|
||||
return;
|
||||
@@ -274,7 +379,7 @@ fn read_mbc5(
|
||||
|
||||
pub fn read(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -300,14 +405,15 @@ pub fn write(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if !cart.enabled {
|
||||
return;
|
||||
}
|
||||
match cart.cart_type {
|
||||
CartType::MBC1RamBatt => write_mbc1(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC5RamBatt => write_mbc5(pif_ram, cart, address, data, size),
|
||||
_ => panic!("Unsupported cart type"),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::ui;
|
||||
pub const MEMPAK_SIZE: usize = 0x8000;
|
||||
const MPK_PAGE_SIZE: usize = 256;
|
||||
|
||||
fn format_mempak(device: &mut device::Device) {
|
||||
pub fn format_mempak(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.mempak.data.len() < MEMPAK_SIZE * 4 {
|
||||
device
|
||||
.ui
|
||||
@@ -43,6 +43,11 @@ fn format_mempak(device: &mut device::Device) {
|
||||
];
|
||||
for i in 0..4 {
|
||||
let offset = i * MEMPAK_SIZE;
|
||||
if device.ui.storage.saves.mempak.data[offset..offset + MEMPAK_SIZE] != [0; MEMPAK_SIZE]
|
||||
{
|
||||
// don't overwrite existing data
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Fill Page 0 with pre-initialized content */
|
||||
device.ui.storage.saves.mempak.data[offset..offset + MPK_PAGE_SIZE]
|
||||
@@ -77,8 +82,6 @@ fn format_mempak(device: &mut device::Device) {
|
||||
|
||||
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
|
||||
if (address as usize) < MEMPAK_SIZE {
|
||||
format_mempak(device);
|
||||
|
||||
let offset = (channel * MEMPAK_SIZE) + address as usize;
|
||||
device.pif.ram[data..data + size]
|
||||
.copy_from_slice(&device.ui.storage.saves.mempak.data[offset..offset + size])
|
||||
@@ -91,8 +94,6 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
|
||||
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
|
||||
if (address as usize) < MEMPAK_SIZE {
|
||||
format_mempak(device);
|
||||
|
||||
let offset = (channel * MEMPAK_SIZE) + address as usize;
|
||||
device.ui.storage.saves.mempak.data[offset..offset + size]
|
||||
.copy_from_slice(&device.pif.ram[data..data + size]);
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
let rumble = device.pif.ram[data + size - 1];
|
||||
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == channel {
|
||||
if netplay.player_number == channel {
|
||||
device::ui::input::set_rumble(&device.ui, 0, rumble);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::device;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TransferPak {
|
||||
pub enabled: bool,
|
||||
pub reset_state: u8,
|
||||
@@ -54,14 +54,14 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
0xC..=0xF => {
|
||||
device::controller::gbcart::read(
|
||||
&mut device.pif.ram,
|
||||
&pak.cart,
|
||||
&mut pak.cart,
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!("unknown transfer pak read {:x}", address >> 12);
|
||||
eprintln!("unknown transfer pak read {:x}", address >> 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
}
|
||||
pak.enabled = true;
|
||||
}
|
||||
_ => println!("unknown transfer pak value"),
|
||||
_ => eprintln!("unknown transfer pak value"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -124,10 +124,11 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
device.vi.elapsed_time as i64,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!("unknown transfer pak write {:x}", address >> 12);
|
||||
eprintln!("unknown transfer pak write {:x}", address >> 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::device;
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
use crate::ui;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const JCMD_VRU_READ: u8 = 0x09;
|
||||
const JCMD_VRU_WRITE: u8 = 0x0A;
|
||||
@@ -18,7 +17,7 @@ const VOICE_STATUS_BUSY: u8 = 0x05;
|
||||
const JDT_VRU: u16 = 0x0100; /* VRU */
|
||||
const CONT_FLAVOR: u16 = JDT_VRU;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vru {
|
||||
pub status: u8,
|
||||
pub voice_state: u8,
|
||||
@@ -28,7 +27,7 @@ pub struct Vru {
|
||||
pub word_buffer: [u16; 40],
|
||||
pub words: Vec<String>,
|
||||
pub talking: bool,
|
||||
pub word_mappings: HashMap<String, String>,
|
||||
pub word_mappings: rustc_hash::FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
@@ -181,12 +180,12 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
|
||||
}
|
||||
JCMD_VRU_READ => {
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
|
||||
#[cfg(not(feature = "gui"))]
|
||||
#[cfg(any(not(feature = "gui"), target_os = "android"))]
|
||||
let index = 0x7FFF;
|
||||
let num_results = if index == 0x7FFF { 0 } else { 1 };
|
||||
let data: HashMap<usize, u16> = HashMap::from([
|
||||
let data: rustc_hash::FxHashMap<usize, u16> = rustc_hash::FxHashMap::from_iter([
|
||||
(0, 0x8000),
|
||||
(2, 0x0F00),
|
||||
(4, 0), // error flags
|
||||
@@ -247,7 +246,7 @@ pub fn vru_talking_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn create_word_mappings(device: &mut device::Device) {
|
||||
device.vru.word_mappings = HashMap::from([
|
||||
device.vru.word_mappings = rustc_hash::FxHashMap::from_iter([
|
||||
(
|
||||
String::from("03A50024000303CF00A80003036000EA"),
|
||||
String::from("pikachu"),
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
use crate::{device, savestates};
|
||||
|
||||
pub const COP0_INDEX_REG: u32 = 0;
|
||||
const COP0_RANDOM_REG: u32 = 1;
|
||||
pub const COP0_ENTRYLO0_REG: u32 = 2;
|
||||
pub const COP0_ENTRYLO1_REG: u32 = 3;
|
||||
pub const COP0_CONTEXT_REG: u32 = 4;
|
||||
pub const COP0_PAGEMASK_REG: u32 = 5;
|
||||
const COP0_WIRED_REG: u32 = 6;
|
||||
//const COP0_UNUSED_7: u32 = 7;
|
||||
pub const COP0_BADVADDR_REG: u32 = 8;
|
||||
pub const COP0_COUNT_REG: u32 = 9;
|
||||
pub const COP0_ENTRYHI_REG: u32 = 10;
|
||||
const COP0_COMPARE_REG: u32 = 11;
|
||||
pub const COP0_STATUS_REG: u32 = 12;
|
||||
pub const COP0_CAUSE_REG: u32 = 13;
|
||||
pub const COP0_EPC_REG: u32 = 14;
|
||||
const COP0_PREVID_REG: u32 = 15;
|
||||
const COP0_CONFIG_REG: u32 = 16;
|
||||
pub const COP0_LLADDR_REG: u32 = 17;
|
||||
//const COP0_WATCHLO_REG: u32 = 18;
|
||||
//const COP0_WATCHHI_REG: u32 = 19;
|
||||
pub const COP0_XCONTEXT_REG: u32 = 20;
|
||||
//const COP0_UNUSED_21: u32 = 21;
|
||||
//const COP0_UNUSED_22: u32 = 22;
|
||||
//const COP0_UNUSED_23: u32 = 23;
|
||||
//const COP0_UNUSED_24: u32 = 24;
|
||||
//const COP0_UNUSED_25: u32 = 25;
|
||||
//const COP0_PARITYERR_REG: u32 = 26;
|
||||
//const COP0_CACHEERR_REG: u32 = 27;
|
||||
pub const COP0_TAGLO_REG: u32 = 28;
|
||||
//const COP0_TAGHI_REG: u32 = 29;
|
||||
pub const COP0_ERROREPC_REG: u32 = 30;
|
||||
//const COP0_UNUSED_31: u32 = 31;
|
||||
pub const COP0_REGS_COUNT: u32 = 32;
|
||||
pub const COP0_INDEX_REG: usize = 0;
|
||||
const COP0_RANDOM_REG: usize = 1;
|
||||
pub const COP0_ENTRYLO0_REG: usize = 2;
|
||||
pub const COP0_ENTRYLO1_REG: usize = 3;
|
||||
pub const COP0_CONTEXT_REG: usize = 4;
|
||||
pub const COP0_PAGEMASK_REG: usize = 5;
|
||||
const COP0_WIRED_REG: usize = 6;
|
||||
//const COP0_UNUSED_7: usize = 7;
|
||||
pub const COP0_BADVADDR_REG: usize = 8;
|
||||
pub const COP0_COUNT_REG: usize = 9;
|
||||
pub const COP0_ENTRYHI_REG: usize = 10;
|
||||
const COP0_COMPARE_REG: usize = 11;
|
||||
pub const COP0_STATUS_REG: usize = 12;
|
||||
pub const COP0_CAUSE_REG: usize = 13;
|
||||
pub const COP0_EPC_REG: usize = 14;
|
||||
const COP0_PREVID_REG: usize = 15;
|
||||
const COP0_CONFIG_REG: usize = 16;
|
||||
pub const COP0_LLADDR_REG: usize = 17;
|
||||
//const COP0_WATCHLO_REG: usize = 18;
|
||||
//const COP0_WATCHHI_REG: usize = 19;
|
||||
pub const COP0_XCONTEXT_REG: usize = 20;
|
||||
//const COP0_UNUSED_21: usize = 21;
|
||||
//const COP0_UNUSED_22: usize = 22;
|
||||
//const COP0_UNUSED_23: usize = 23;
|
||||
//const COP0_UNUSED_24: usize = 24;
|
||||
//const COP0_UNUSED_25: usize = 25;
|
||||
//const COP0_PARITYERR_REG: usize = 26;
|
||||
//const COP0_CACHEERR_REG: usize = 27;
|
||||
pub const COP0_TAGLO_REG: usize = 28;
|
||||
//const COP0_TAGHI_REG: usize = 29;
|
||||
pub const COP0_ERROREPC_REG: usize = 30;
|
||||
//const COP0_UNUSED_31: usize = 31;
|
||||
pub const COP0_REGS_COUNT: usize = 32;
|
||||
|
||||
pub const COP0_STATUS_IE: u64 = 1 << 0;
|
||||
pub const COP0_STATUS_EXL: u64 = 1 << 1;
|
||||
@@ -91,17 +91,17 @@ const COP0_XCONTEXT_REG_MASK: u64 =
|
||||
const COP0_PARITYERR_REG_MASK: u64 = 0b00000000000000000000000011111111;
|
||||
const COP0_TAGLO_REG_MASK: u64 = 0b00001111111111111111111111000000;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop0 {
|
||||
pub reg_latch: u64,
|
||||
pub regs: [u64; COP0_REGS_COUNT as usize],
|
||||
pub reg_write_masks: [u64; COP0_REGS_COUNT as usize],
|
||||
pub regs: [u64; COP0_REGS_COUNT],
|
||||
pub reg_write_masks: [u64; COP0_REGS_COUNT],
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs: [fn(&mut device::Device, u32); 32],
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs2: [fn(&mut device::Device, u32); 32],
|
||||
pub tlb_lut_r: Vec<device::tlb::TlbLut>,
|
||||
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
|
||||
pub tlb_lut_r: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
|
||||
pub tlb_lut_w: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
|
||||
pub tlb_entries: [device::tlb::TlbEntry; 32],
|
||||
pub is_event: bool,
|
||||
}
|
||||
@@ -136,11 +136,11 @@ fn dmtc0(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn tlbr(device: &mut device::Device, _opcode: u32) {
|
||||
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
|
||||
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG])
|
||||
}
|
||||
|
||||
fn tlbwi(device: &mut device::Device, _opcode: u32) {
|
||||
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
|
||||
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG])
|
||||
}
|
||||
|
||||
fn tlbwr(device: &mut device::Device, _opcode: u32) {
|
||||
@@ -153,12 +153,12 @@ fn tlbp(device: &mut device::Device, _opcode: u32) {
|
||||
}
|
||||
|
||||
fn eret(device: &mut device::Device, _opcode: u32) {
|
||||
if device.cpu.cop0.regs[COP0_STATUS_REG as usize] & COP0_STATUS_ERL != 0 {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG as usize];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_ERL
|
||||
if device.cpu.cop0.regs[COP0_STATUS_REG] & COP0_STATUS_ERL != 0 {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_ERL
|
||||
} else {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG as usize];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_EXL
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_EXL
|
||||
}
|
||||
device.cpu.branch_state.state = device::cpu::State::Exception;
|
||||
device.cpu.llbit = false;
|
||||
@@ -184,7 +184,7 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
|
||||
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
|
||||
|
||||
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
|
||||
match index {
|
||||
match index as usize {
|
||||
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
|
||||
COP0_RANDOM_REG => set_random_register(device),
|
||||
7 | 21 | 22 | 23 | 24 | 25 | 31 => device.cpu.cop0.reg_latch,
|
||||
@@ -194,22 +194,18 @@ fn get_control_registers(device: &device::Device, index: u32) -> u64 {
|
||||
|
||||
fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64) {
|
||||
device.cpu.cop0.reg_latch = data;
|
||||
match index {
|
||||
match index as usize {
|
||||
COP0_COUNT_REG => {
|
||||
data &= 0xFFFFFFFF;
|
||||
data <<= 1;
|
||||
device::events::translate_events(
|
||||
device,
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize],
|
||||
data,
|
||||
);
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize] = data;
|
||||
device::events::translate_events(device, device.cpu.cop0.regs[COP0_COUNT_REG], data);
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG] = data;
|
||||
return;
|
||||
}
|
||||
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 31,
|
||||
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG] = 31,
|
||||
COP0_COMPARE_REG => {
|
||||
data &= 0xFFFFFFFF;
|
||||
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG as usize] >> 1) & 0xFFFFFFFF;
|
||||
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG] >> 1) & 0xFFFFFFFF;
|
||||
let mut compare_event_diff = (data as u32).wrapping_sub(current_count as u32);
|
||||
|
||||
if compare_event_diff == 0 {
|
||||
@@ -221,7 +217,7 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
device::events::EVENT_TYPE_COMPARE,
|
||||
(compare_event_diff as u64) << 1,
|
||||
);
|
||||
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[COP0_CAUSE_REG] &= !COP0_CAUSE_IP7;
|
||||
}
|
||||
COP0_STATUS_REG
|
||||
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
|
||||
@@ -239,9 +235,8 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
}
|
||||
|
||||
pub fn compare_event(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
|
||||
device::events::create_event_at(
|
||||
device,
|
||||
@@ -252,17 +247,17 @@ pub fn compare_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn set_random_register(device: &device::Device) -> u64 {
|
||||
if device.cpu.cop0.regs[COP0_WIRED_REG as usize] > 31 {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize]) & 0x3F
|
||||
if device.cpu.cop0.regs[COP0_WIRED_REG] > 31 {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG]) & 0x3F
|
||||
} else {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize])
|
||||
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG as usize])
|
||||
+ device.cpu.cop0.regs[COP0_WIRED_REG as usize]
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG])
|
||||
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG])
|
||||
+ device.cpu.cop0.regs[COP0_WIRED_REG]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cycles(device: &mut device::Device, cycles: u64) {
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
|
||||
}
|
||||
|
||||
pub fn map_instructions(device: &mut device::Device) {
|
||||
@@ -376,14 +371,14 @@ pub fn init(device: &mut device::Device) {
|
||||
map_instructions(device);
|
||||
|
||||
// taken from VR4300 manual
|
||||
device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 0b00000000000000000000000000011111;
|
||||
device.cpu.cop0.regs[COP0_CONFIG_REG as usize] = 0b01110000000001101110010001100000;
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] = 0b00000000010000000000000000000100;
|
||||
device.cpu.cop0.regs[COP0_PREVID_REG as usize] = 0b00000000000000000000101100100010;
|
||||
device.cpu.cop0.regs[COP0_EPC_REG as usize] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_ERROREPC_REG as usize] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_BADVADDR_REG as usize] = 0xFFFFFFFF;
|
||||
device.cpu.cop0.regs[COP0_CONTEXT_REG as usize] = 0x7FFFF0;
|
||||
device.cpu.cop0.regs[COP0_RANDOM_REG] = 0b00000000000000000000000000011111;
|
||||
device.cpu.cop0.regs[COP0_CONFIG_REG] = 0b01110000000001101110010001100000;
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] = 0b00000000010000000000000000000100;
|
||||
device.cpu.cop0.regs[COP0_PREVID_REG] = 0b00000000000000000000101100100010;
|
||||
device.cpu.cop0.regs[COP0_EPC_REG] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_ERROREPC_REG] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_BADVADDR_REG] = 0xFFFFFFFF;
|
||||
device.cpu.cop0.regs[COP0_CONTEXT_REG] = 0x7FFFF0;
|
||||
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_COMPARE, u32::MAX as u64)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const FCR31_CAUSE_MASK: u32 = 0b00000000000000111111000000000000;
|
||||
const FCR31_ENABLE_MASK: u32 = 0b00000000000000000000111110000000;
|
||||
const FCR31_WRITE_MASK: u32 = 0b00000001100000111111111111111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop1 {
|
||||
pub fcr0: u32,
|
||||
pub fcr31: u32,
|
||||
@@ -47,9 +47,7 @@ pub struct Cop1 {
|
||||
}
|
||||
|
||||
pub fn lwc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -79,9 +77,7 @@ pub fn lwc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn ldc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -118,9 +114,7 @@ pub fn ldc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn swc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -145,9 +139,7 @@ pub fn swc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn sdc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -181,9 +173,7 @@ pub fn sdc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -193,9 +183,7 @@ fn mfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -205,9 +193,7 @@ fn dmfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn cfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -217,9 +203,7 @@ fn cfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dcfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -229,9 +213,7 @@ fn dcfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mtc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -247,9 +229,7 @@ fn mtc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmtc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -260,9 +240,7 @@ fn dmtc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn ctc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -274,9 +252,7 @@ fn ctc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dctc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -286,9 +262,7 @@ fn dctc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -296,9 +270,7 @@ fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -306,9 +278,7 @@ fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -316,9 +286,7 @@ fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -326,9 +294,7 @@ fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_w(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -340,9 +306,7 @@ fn unusable(device: &mut device::Device, _opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn reserved(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -406,9 +370,7 @@ fn set_control_registers_fpu(device: &mut device::Device, index: u32, data: u32)
|
||||
|
||||
pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, clear_high: bool) {
|
||||
let bytes = value.to_ne_bytes();
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
device.cpu.cop1.fgr32[index] = bytes;
|
||||
} else {
|
||||
let bytes_lo = bytes;
|
||||
@@ -422,9 +384,7 @@ pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, cle
|
||||
}
|
||||
|
||||
pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
f32::from_ne_bytes(device.cpu.cop1.fgr32[index])
|
||||
} else {
|
||||
f32::from_ne_bytes(device.cpu.cop1.fgr64[index][0..4].try_into().unwrap())
|
||||
@@ -433,9 +393,7 @@ pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
|
||||
|
||||
pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
|
||||
let bytes = value.to_ne_bytes();
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
device.cpu.cop1.fgr32[index & !1] = bytes[0..4].try_into().unwrap();
|
||||
device.cpu.cop1.fgr32[(index & !1) + 1] = bytes[4..8].try_into().unwrap();
|
||||
} else {
|
||||
@@ -444,9 +402,7 @@ pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
|
||||
}
|
||||
|
||||
pub fn get_fpr_double(device: &device::Device, index: usize) -> f64 {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
let bytes_lo = device.cpu.cop1.fgr32[index & !1];
|
||||
let bytes_hi = device.cpu.cop1.fgr32[(index & !1) + 1];
|
||||
f64::from_ne_bytes([bytes_lo, bytes_hi].concat().try_into().unwrap())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{device, savestates};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop2 {
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs: [fn(&mut device::Device, u32); 32],
|
||||
@@ -8,9 +8,7 @@ pub struct Cop2 {
|
||||
}
|
||||
|
||||
fn mfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
|
||||
@@ -18,18 +16,14 @@ fn mfc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] = device.cpu.cop2.reg_latch
|
||||
}
|
||||
|
||||
fn cfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
|
||||
@@ -37,27 +31,21 @@ fn cfc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mtc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
|
||||
}
|
||||
|
||||
fn dmtc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
|
||||
}
|
||||
|
||||
fn ctc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -65,9 +53,7 @@ fn ctc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn reserved(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum State {
|
||||
Step,
|
||||
Take,
|
||||
@@ -11,13 +11,13 @@ pub enum State {
|
||||
Exception,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BranchState {
|
||||
pub state: State,
|
||||
pub pc: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cpu {
|
||||
pub cop0: device::cop0::Cop0,
|
||||
pub cop1: device::cop1::Cop1,
|
||||
@@ -227,7 +227,10 @@ pub fn init(device: &mut device::Device) {
|
||||
device.cpu.clock_rate = if !device.cpu.overclock {
|
||||
93750000
|
||||
} else {
|
||||
ui::video::onscreen_message("Overclocking enabled, setting clock rate to 125 MHz", false);
|
||||
ui::video::onscreen_message(
|
||||
"Overclocking enabled, setting clock rate to 125 MHz",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
125000000
|
||||
};
|
||||
|
||||
@@ -297,8 +300,7 @@ pub fn run(device: &mut device::Device) {
|
||||
State::Exception => device.cpu.branch_state.state = State::Step,
|
||||
}
|
||||
device::cop0::add_cycles(device, 1);
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] > device.cpu.next_event_count
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] > device.cpu.next_event_count {
|
||||
device::events::trigger_event(device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub fn check_relative_idle_loop(device: &mut device::Device, opcode: u32) {
|
||||
device::memory::AccessSize::Word,
|
||||
) == 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ fn check_absolute_idle_loop(device: &mut device::Device, target: u64) {
|
||||
device::memory::AccessSize::Word,
|
||||
) == 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,17 +647,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
valid = 1
|
||||
}
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
valid << 7,
|
||||
0x80,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
0,
|
||||
0x40,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
(device.memory.icache[icache_line].tag >> 4) as u64,
|
||||
0xFFFFF00,
|
||||
)
|
||||
@@ -665,10 +665,9 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
0x08 => {
|
||||
//icache store tag
|
||||
device.memory.icache[icache_line].valid =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
|
||||
device.memory.icache[icache_line].tag =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
|
||||
as u32
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
|
||||
}
|
||||
0x10 => {
|
||||
//icache hit invalidate
|
||||
@@ -704,17 +703,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
dirty = 1
|
||||
}
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
valid << 7,
|
||||
0x80,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
dirty << 6,
|
||||
0x40,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
(device.memory.dcache[dcache_line].tag >> 4) as u64,
|
||||
0xFFFFF00,
|
||||
)
|
||||
@@ -722,12 +721,11 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
0x09 => {
|
||||
//dcache index store tag
|
||||
device.memory.dcache[dcache_line].valid =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
|
||||
device.memory.dcache[dcache_line].dirty =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x40) >> 6 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x40) >> 6 != 0;
|
||||
device.memory.dcache[dcache_line].tag =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
|
||||
as u32
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
|
||||
}
|
||||
0x0D => {
|
||||
//dcache create dirty exclusive
|
||||
@@ -765,7 +763,7 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown cache code {:#01x}", rt(opcode))
|
||||
panic!("unknown cache code {:x}", rt(opcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +785,7 @@ pub fn ll(device: &mut device::Device, opcode: u32) {
|
||||
cached,
|
||||
) as i32);
|
||||
device.cpu.llbit = true;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
|
||||
}
|
||||
|
||||
pub fn lld(device: &mut device::Device, opcode: u32) {
|
||||
@@ -816,7 +814,7 @@ pub fn lld(device: &mut device::Device, opcode: u32) {
|
||||
device.cpu.gpr[rt(opcode) as usize] = ((w[0] as u64) << 32) | (w[1]) as u64;
|
||||
|
||||
device.cpu.llbit = true;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
|
||||
}
|
||||
|
||||
pub fn ld(device: &mut device::Device, opcode: u32) {
|
||||
|
||||
@@ -15,7 +15,8 @@ pub const EVENT_TYPE_VRU: usize = 10;
|
||||
pub const EVENT_TYPE_PAK: usize = 11;
|
||||
pub const EVENT_TYPE_SAVE: usize = 12;
|
||||
pub const EVENT_TYPE_NMI: usize = 13;
|
||||
pub const EVENT_TYPE_COUNT: usize = 14;
|
||||
pub const EVENT_TYPE_UPDATE_SCREEN: usize = 14;
|
||||
pub const EVENT_TYPE_COUNT: usize = 15;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Event {
|
||||
@@ -26,7 +27,7 @@ pub struct Event {
|
||||
pub fn create_event(device: &mut device::Device, name: usize, when: u64) {
|
||||
device.cpu.events[name] = Event {
|
||||
enabled: true,
|
||||
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] + when,
|
||||
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] + when,
|
||||
};
|
||||
set_next_event(device);
|
||||
}
|
||||
@@ -65,6 +66,7 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
|
||||
EVENT_TYPE_PAK => device::controller::pak_switch_event,
|
||||
EVENT_TYPE_SAVE => ui::storage::save_event,
|
||||
EVENT_TYPE_NMI => device::exceptions::reset_event,
|
||||
EVENT_TYPE_UPDATE_SCREEN => device::vi::update_screen,
|
||||
_ => dummy_event,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
pub fn check_pending_interrupts(device: &mut device::Device) {
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
|
||||
& (device::cop0::COP0_STATUS_IE
|
||||
| device::cop0::COP0_STATUS_EXL
|
||||
| device::cop0::COP0_STATUS_ERL))
|
||||
@@ -12,17 +12,14 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if device.mi.regs[device::mi::MI_INTR_REG as usize]
|
||||
& device.mi.regs[device::mi::MI_INTR_MASK_REG as usize]
|
||||
!= 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP2;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
if device.mi.regs[device::mi::MI_INTR_REG] & device.mi.regs[device::mi::MI_INTR_MASK_REG] != 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP2;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
}
|
||||
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize]
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG]
|
||||
& device::cop0::COP0_CAUSE_IP_MASK)
|
||||
== 0
|
||||
{
|
||||
@@ -42,43 +39,37 @@ pub fn interrupt_exception(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
pub fn floating_point_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_FPE;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_FPE;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn trap_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TR;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TR;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn syscall_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_SYS;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_SYS;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn break_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_BP;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_BP;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn reserved_exception(device: &mut device::Device, cop: u64) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn cop_unusable_exception(device: &mut device::Device, cop: u64) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
@@ -89,31 +80,29 @@ pub fn tlb_miss_exception(
|
||||
access_type: device::memory::AccessType,
|
||||
) {
|
||||
if access_type == device::memory::AccessType::Read {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TLBL
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBL
|
||||
} else {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TLBS
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBS
|
||||
}
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG as usize] = address;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG] = address;
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG],
|
||||
address >> 9,
|
||||
device::cop0::COP0_CONTEXT_BADVPN2_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
|
||||
address >> 9,
|
||||
device::cop0::COP0_XCONTEXT_BADVPN2_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
|
||||
address >> 31,
|
||||
device::cop0::COP0_XCONTEXT_REGION_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG],
|
||||
address,
|
||||
0xFFFFE000,
|
||||
);
|
||||
@@ -124,7 +113,7 @@ pub fn tlb_miss_exception(
|
||||
if address & !3 >= i.start_even && address & !3 <= i.end_even {
|
||||
valid = i.v_even != 0;
|
||||
if valid && access_type == device::memory::AccessType::Write && i.d_even == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_MOD;
|
||||
valid = false;
|
||||
}
|
||||
@@ -133,7 +122,7 @@ pub fn tlb_miss_exception(
|
||||
if address & !3 >= i.start_odd && address & !3 <= i.end_odd {
|
||||
valid = i.v_odd != 0;
|
||||
if valid && access_type == device::memory::AccessType::Write && i.d_odd == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_MOD;
|
||||
valid = false;
|
||||
}
|
||||
@@ -141,8 +130,7 @@ pub fn tlb_miss_exception(
|
||||
}
|
||||
}
|
||||
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
|
||||
== 0
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0
|
||||
&& valid
|
||||
{
|
||||
vector_offset = 0;
|
||||
@@ -152,43 +140,37 @@ pub fn tlb_miss_exception(
|
||||
}
|
||||
|
||||
pub fn reset_event(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &= !device::cop0::COP0_CAUSE_IP4;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP4;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_ERL
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_ERL
|
||||
| device::cop0::COP0_STATUS_SR
|
||||
| device::cop0::COP0_STATUS_BEV;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] &= !device::cop0::COP0_STATUS_TS;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] &= !device::cop0::COP0_STATUS_TS;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG as usize] = device.cpu.pc;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG] = device.cpu.pc;
|
||||
device.cpu.pc = 0xBFC00000;
|
||||
device.cpu.branch_state.state = device::cpu::State::Step;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] = 0;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] = 0;
|
||||
|
||||
device::pif::reset_pif(device, true);
|
||||
|
||||
ui::video::onscreen_message("Game reset", false);
|
||||
ui::video::onscreen_message("Game reset", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
fn exception_general(device: &mut device::Device, vector_offset: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
|
||||
== 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] = device.cpu.pc;
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] = device.cpu.pc;
|
||||
if device::cpu::in_delay_slot(device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |=
|
||||
device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] -= 4;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] -= 4;
|
||||
} else {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_BD;
|
||||
}
|
||||
}
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_EXL;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_EXL;
|
||||
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_BEV
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_BEV == 0 {
|
||||
device.cpu.pc = device::cpu_instructions::se32((0x80000000 + vector_offset) as i32);
|
||||
} else {
|
||||
device.cpu.pc = device::cpu_instructions::se32((0xBFC00200 + vector_offset) as i32);
|
||||
|
||||
@@ -38,7 +38,7 @@ pub enum AccessSize {
|
||||
Icache = 32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Memory {
|
||||
#[serde(skip, default = "savestates::default_memory_read_fast")]
|
||||
pub fast_read: [fn(&device::Device, u64, AccessSize) -> u32; 0x2000], // fast_read is used for lookups that try to detect idle loops
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::device;
|
||||
|
||||
pub const MI_INIT_MODE_REG: u32 = 0;
|
||||
const MI_VERSION_REG: u32 = 1;
|
||||
pub const MI_INTR_REG: u32 = 2;
|
||||
pub const MI_INTR_MASK_REG: u32 = 3;
|
||||
pub const MI_REGS_COUNT: u32 = 4;
|
||||
pub const MI_INIT_MODE_REG: usize = 0;
|
||||
const MI_VERSION_REG: usize = 1;
|
||||
pub const MI_INTR_REG: usize = 2;
|
||||
pub const MI_INTR_MASK_REG: usize = 3;
|
||||
pub const MI_REGS_COUNT: usize = 4;
|
||||
|
||||
/* read */
|
||||
pub const MI_INTR_SP: u32 = 1 << 0;
|
||||
@@ -44,9 +44,9 @@ const MI_SET_RDRAM: u32 = 1 << 13;
|
||||
|
||||
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Mi {
|
||||
pub regs: [u32; MI_REGS_COUNT as usize],
|
||||
pub regs: [u32; MI_REGS_COUNT],
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
@@ -60,46 +60,45 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
MI_INIT_MODE_REG => update_init_mode(device, value),
|
||||
MI_INTR_MASK_REG => update_intr_mask(device, value),
|
||||
_ => device::memory::masked_write_32(&mut device.mi.regs[reg as usize], value, mask),
|
||||
}
|
||||
|
||||
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_IP2;
|
||||
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
|
||||
}
|
||||
device::exceptions::check_pending_interrupts(device)
|
||||
}
|
||||
|
||||
fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= w & MI_INIT_LENGTH_MASK;
|
||||
|
||||
if w & MI_CLR_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_MODE
|
||||
}
|
||||
if w & MI_SET_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_INIT_MODE
|
||||
}
|
||||
if w & MI_CLR_EBUS != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_EBUS_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_EBUS_MODE
|
||||
}
|
||||
if w & MI_SET_EBUS != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_EBUS_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_EBUS_MODE
|
||||
}
|
||||
if w & MI_CLR_RDRAM != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_RDRAM_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_RDRAM_MODE
|
||||
}
|
||||
if w & MI_SET_RDRAM != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_RDRAM_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_RDRAM_MODE
|
||||
}
|
||||
|
||||
if w & MI_CLR_DP_INTR != 0 {
|
||||
@@ -109,57 +108,56 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
|
||||
fn update_intr_mask(device: &mut device::Device, w: u32) {
|
||||
if w & MI_CLR_SP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SP
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SP
|
||||
}
|
||||
if w & MI_SET_SP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SP
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SP
|
||||
}
|
||||
if w & MI_CLR_SI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SI
|
||||
}
|
||||
if w & MI_SET_SI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SI
|
||||
}
|
||||
if w & MI_CLR_AI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_AI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_AI
|
||||
}
|
||||
if w & MI_SET_AI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_AI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_AI
|
||||
}
|
||||
if w & MI_CLR_VI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_VI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_VI
|
||||
}
|
||||
if w & MI_SET_VI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_VI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_VI
|
||||
}
|
||||
if w & MI_CLR_PI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_PI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_PI
|
||||
}
|
||||
if w & MI_SET_PI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_PI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_PI
|
||||
}
|
||||
if w & MI_CLR_DP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_DP
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_DP
|
||||
}
|
||||
if w & MI_SET_DP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_DP
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_DP
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
|
||||
device.mi.regs[MI_INTR_REG as usize] &= !interrupt;
|
||||
device.mi.regs[MI_INTR_REG] &= !interrupt;
|
||||
|
||||
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_IP2;
|
||||
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
|
||||
device.mi.regs[MI_INTR_REG as usize] |= interrupt;
|
||||
device.mi.regs[MI_INTR_REG] |= interrupt;
|
||||
device::exceptions::check_pending_interrupts(device)
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.mi.regs[MI_VERSION_REG as usize] = 0x02020102
|
||||
device.mi.regs[MI_VERSION_REG] = 0x02020102
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@ use crate::device;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
const PI_CART_ADDR_REG: u32 = 1;
|
||||
const PI_RD_LEN_REG: u32 = 2;
|
||||
const PI_WR_LEN_REG: u32 = 3;
|
||||
pub const PI_STATUS_REG: u32 = 4;
|
||||
const PI_BSD_DOM1_LAT_REG: u32 = 5;
|
||||
const PI_BSD_DOM1_PWD_REG: u32 = 6;
|
||||
const PI_BSD_DOM1_PGS_REG: u32 = 7;
|
||||
const PI_BSD_DOM1_RLS_REG: u32 = 8;
|
||||
const PI_BSD_DOM2_LAT_REG: u32 = 9;
|
||||
const PI_BSD_DOM2_PWD_REG: u32 = 10;
|
||||
const PI_BSD_DOM2_PGS_REG: u32 = 11;
|
||||
const PI_BSD_DOM2_RLS_REG: u32 = 12;
|
||||
//const UNKNOWN_REG: u32 = 13; //LibDragon
|
||||
pub const PI_REGS_COUNT: u32 = 14;
|
||||
const PI_DRAM_ADDR_REG: usize = 0;
|
||||
const PI_CART_ADDR_REG: usize = 1;
|
||||
const PI_RD_LEN_REG: usize = 2;
|
||||
const PI_WR_LEN_REG: usize = 3;
|
||||
pub const PI_STATUS_REG: usize = 4;
|
||||
const PI_BSD_DOM1_LAT_REG: usize = 5;
|
||||
const PI_BSD_DOM1_PWD_REG: usize = 6;
|
||||
const PI_BSD_DOM1_PGS_REG: usize = 7;
|
||||
const PI_BSD_DOM1_RLS_REG: usize = 8;
|
||||
const PI_BSD_DOM2_LAT_REG: usize = 9;
|
||||
const PI_BSD_DOM2_PWD_REG: usize = 10;
|
||||
const PI_BSD_DOM2_PGS_REG: usize = 11;
|
||||
const PI_BSD_DOM2_RLS_REG: usize = 12;
|
||||
//const UNKNOWN_REG: usize = 13; //LibDragon
|
||||
pub const PI_REGS_COUNT: usize = 14;
|
||||
|
||||
/* PI_STATUS - read */
|
||||
const PI_STATUS_DMA_BUSY: u32 = 1 << 0;
|
||||
@@ -28,9 +28,9 @@ const PI_STATUS_INTERRUPT: u32 = 1 << 3;
|
||||
const PI_STATUS_RESET: u32 = 1 << 0;
|
||||
const PI_STATUS_CLR_INTR: u32 = 1 << 1;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Pi {
|
||||
pub regs: [u32; PI_REGS_COUNT as usize],
|
||||
pub regs: [u32; PI_REGS_COUNT],
|
||||
}
|
||||
|
||||
struct PiHandler {
|
||||
@@ -45,7 +45,7 @@ pub fn read_regs(
|
||||
) -> u32 {
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
PI_WR_LEN_REG | PI_RD_LEN_REG => 0x7F,
|
||||
PI_CART_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFFFE,
|
||||
PI_DRAM_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFE,
|
||||
@@ -54,11 +54,11 @@ pub fn read_regs(
|
||||
}
|
||||
|
||||
fn dma_read(device: &mut device::Device) {
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
|
||||
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_RD_LEN_REG as usize] & 0xFFFFFF) + 1;
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_RD_LEN_REG] & 0xFFFFFF) + 1;
|
||||
|
||||
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
|
||||
if length >= 0x7f && (length & 1) != 0 {
|
||||
@@ -70,20 +70,18 @@ fn dma_read(device: &mut device::Device) {
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
|
||||
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
|
||||
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
|
||||
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
|
||||
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
|
||||
}
|
||||
|
||||
fn dma_write(device: &mut device::Device) {
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
|
||||
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_WR_LEN_REG as usize] & 0xFFFFFF) + 1;
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_WR_LEN_REG] & 0xFFFFFF) + 1;
|
||||
|
||||
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
|
||||
if length >= 0x7f && (length & 1) != 0 {
|
||||
@@ -101,12 +99,10 @@ fn dma_write(device: &mut device::Device) {
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
|
||||
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
|
||||
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
|
||||
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
|
||||
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
|
||||
}
|
||||
|
||||
fn get_handler(address: u32) -> PiHandler {
|
||||
@@ -138,7 +134,7 @@ fn get_handler(address: u32) -> PiHandler {
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
PI_RD_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.pi.regs[reg as usize], value, mask);
|
||||
device::pi::dma_read(device)
|
||||
@@ -153,7 +149,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_PI);
|
||||
}
|
||||
if value & mask & PI_STATUS_RESET != 0 {
|
||||
device.pi.regs[PI_STATUS_REG as usize] = 0
|
||||
device.pi.regs[PI_STATUS_REG] = 0
|
||||
}
|
||||
}
|
||||
PI_BSD_DOM1_LAT_REG | PI_BSD_DOM1_PWD_REG | PI_BSD_DOM2_LAT_REG | PI_BSD_DOM2_PWD_REG => {
|
||||
@@ -179,15 +175,15 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
|
||||
let page_size_base: f64 = 2.0;
|
||||
|
||||
if domain == 1 {
|
||||
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG as usize] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG as usize] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG as usize] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG as usize] + 2) as f64);
|
||||
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG] + 2) as f64);
|
||||
} else if domain == 2 {
|
||||
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG as usize] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG as usize] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG as usize] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG as usize] + 2) as f64);
|
||||
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG] + 2) as f64);
|
||||
} else {
|
||||
panic!("unknown pi dma")
|
||||
}
|
||||
@@ -201,8 +197,8 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
|
||||
}
|
||||
|
||||
pub fn dma_event(device: &mut device::Device) {
|
||||
device.pi.regs[PI_STATUS_REG as usize] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_INTERRUPT;
|
||||
device.pi.regs[PI_STATUS_REG] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_INTERRUPT;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_PI)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod rom;
|
||||
use crate::device;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Pif {
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub rom: [u8; 1984],
|
||||
@@ -68,7 +68,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
|
||||
device.si.dma_dir = device::si::DmaDir::Write;
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_SI, 3200); //based on https://github.com/rasky/n64-systembench
|
||||
device.si.regs[device::si::SI_STATUS_REG as usize] |=
|
||||
device.si.regs[device::si::SI_STATUS_REG] |=
|
||||
device::si::SI_STATUS_DMA_BUSY | device::si::SI_STATUS_IO_BUSY
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ pub fn process_ram(device: &mut device::Device) {
|
||||
pub fn connect_pif_channels(device: &mut device::Device) {
|
||||
for i in 0..4 {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
if netplay.connected[i] {
|
||||
device.pif.channels[i].process = Some(device::controller::process);
|
||||
}
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
@@ -278,7 +278,7 @@ pub fn init(device: &mut device::Device) {
|
||||
|
||||
for i in 0..4 {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
if netplay.connected[i] {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
}
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use crate::{device, savestates, ui};
|
||||
use crate::{device, ui};
|
||||
|
||||
pub const DPC_START_REG: u32 = 0;
|
||||
pub const DPC_END_REG: u32 = 1;
|
||||
pub const DPC_CURRENT_REG: u32 = 2;
|
||||
pub const DPC_STATUS_REG: u32 = 3;
|
||||
const DPC_CLOCK_REG: u32 = 4;
|
||||
const DPC_BUFBUSY_REG: u32 = 5;
|
||||
const DPC_PIPEBUSY_REG: u32 = 6;
|
||||
const DPC_TMEM_REG: u32 = 7;
|
||||
pub const DPC_REGS_COUNT: u32 = 8;
|
||||
pub const DPC_START_REG: usize = 0;
|
||||
pub const DPC_END_REG: usize = 1;
|
||||
pub const DPC_CURRENT_REG: usize = 2;
|
||||
pub const DPC_STATUS_REG: usize = 3;
|
||||
const DPC_CLOCK_REG: usize = 4;
|
||||
const DPC_BUFBUSY_REG: usize = 5;
|
||||
const DPC_PIPEBUSY_REG: usize = 6;
|
||||
const DPC_TMEM_REG: usize = 7;
|
||||
pub const DPC_REGS_COUNT: usize = 8;
|
||||
|
||||
//const DPS_TBIST_REG: u32 = 0;
|
||||
//const DPS_TEST_MODE_REG: u32 = 1;
|
||||
//const DPS_BUFTEST_ADDR_REG: u32 = 2;
|
||||
//const DPS_BUFTEST_DATA_REG: u32 = 3;
|
||||
pub const DPS_REGS_COUNT: u32 = 4;
|
||||
//const DPS_TBIST_REG: usize = 0;
|
||||
//const DPS_TEST_MODE_REG: usize = 1;
|
||||
//const DPS_BUFTEST_ADDR_REG: usize = 2;
|
||||
//const DPS_BUFTEST_DATA_REG: usize = 3;
|
||||
pub const DPS_REGS_COUNT: usize = 4;
|
||||
|
||||
/* DPC status - read */
|
||||
const DPC_STATUS_XBUS_DMEM_DMA: u32 = 1 << 0;
|
||||
@@ -40,10 +40,10 @@ const DPC_CLR_PIPE_CTR: u32 = 1 << 7;
|
||||
const DPC_CLR_CMD_CTR: u32 = 1 << 8;
|
||||
const DPC_CLR_CLOCK_CTR: u32 = 1 << 9;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rdp {
|
||||
pub regs_dpc: [u32; DPC_REGS_COUNT as usize],
|
||||
pub regs_dps: [u32; DPS_REGS_COUNT as usize],
|
||||
pub regs_dpc: [u32; DPC_REGS_COUNT],
|
||||
pub regs_dps: [u32; DPS_REGS_COUNT],
|
||||
pub wait_frozen: bool,
|
||||
pub last_status_value: u32,
|
||||
}
|
||||
@@ -57,7 +57,7 @@ pub fn read_regs_dpc(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
DPC_STATUS_REG => {
|
||||
let value =
|
||||
device.rdp.regs_dpc[reg as usize] & (DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
|
||||
@@ -80,18 +80,18 @@ pub fn read_regs_dpc(
|
||||
|
||||
pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
DPC_CURRENT_REG | DPC_CLOCK_REG | DPC_BUFBUSY_REG | DPC_PIPEBUSY_REG | DPC_TMEM_REG => {}
|
||||
DPC_STATUS_REG => update_dpc_status(device, value & mask),
|
||||
DPC_START_REG => {
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) == 0 {
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) == 0 {
|
||||
device::memory::masked_write_32(
|
||||
&mut device.rdp.regs_dpc[reg as usize],
|
||||
value & 0xFFFFF8,
|
||||
mask,
|
||||
)
|
||||
}
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_START_VALID
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_START_VALID
|
||||
}
|
||||
DPC_END_REG => {
|
||||
device::memory::masked_write_32(
|
||||
@@ -99,12 +99,11 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
|
||||
value & 0xFFFFF8,
|
||||
mask,
|
||||
);
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) != 0 {
|
||||
device.rdp.regs_dpc[DPC_CURRENT_REG as usize] =
|
||||
device.rdp.regs_dpc[DPC_START_REG as usize];
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_START_VALID
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) != 0 {
|
||||
device.rdp.regs_dpc[DPC_CURRENT_REG] = device.rdp.regs_dpc[DPC_START_REG];
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_START_VALID
|
||||
}
|
||||
if device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_FREEZE == 0 {
|
||||
if device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_FREEZE == 0 {
|
||||
run_rdp(device)
|
||||
} else {
|
||||
device.rdp.wait_frozen = true;
|
||||
@@ -116,20 +115,11 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
|
||||
|
||||
fn run_rdp(device: &mut device::Device) {
|
||||
let timer = ui::video::process_rdp_list();
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |=
|
||||
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
|
||||
|
||||
if timer != 0 {
|
||||
if device.save_state {
|
||||
// Right after full sync, good time for save state
|
||||
device.save_state = false;
|
||||
savestates::create_savestate(device);
|
||||
} else if device.load_state {
|
||||
// Right after full sync, good time for save state
|
||||
device.load_state = false;
|
||||
savestates::load_savestate(device);
|
||||
}
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_DP, timer)
|
||||
}
|
||||
}
|
||||
@@ -154,59 +144,59 @@ pub fn write_regs_dps(device: &mut device::Device, address: u64, value: u32, mas
|
||||
fn update_dpc_status(device: &mut device::Device, w: u32) {
|
||||
/* clear / set xbus_dmem_dma */
|
||||
if w & DPC_CLR_XBUS_DMEM_DMA != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_XBUS_DMEM_DMA
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_XBUS_DMEM_DMA
|
||||
}
|
||||
if w & DPC_SET_XBUS_DMEM_DMA != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_XBUS_DMEM_DMA
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_XBUS_DMEM_DMA
|
||||
}
|
||||
|
||||
/* clear / set freeze */
|
||||
if w & DPC_CLR_FREEZE != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FREEZE;
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FREEZE;
|
||||
if device.rdp.wait_frozen {
|
||||
run_rdp(device);
|
||||
device.rdp.wait_frozen = false;
|
||||
}
|
||||
}
|
||||
if w & DPC_SET_FREEZE != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FREEZE;
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FREEZE;
|
||||
}
|
||||
|
||||
/* clear / set flush */
|
||||
if w & DPC_CLR_FLUSH != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FLUSH
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FLUSH
|
||||
}
|
||||
if w & DPC_SET_FLUSH != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FLUSH
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FLUSH
|
||||
}
|
||||
|
||||
if w & DPC_CLR_TMEM_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_TMEM_BUSY;
|
||||
device.rdp.regs_dpc[DPC_TMEM_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_TMEM_BUSY;
|
||||
device.rdp.regs_dpc[DPC_TMEM_REG] = 0
|
||||
}
|
||||
if w & DPC_CLR_PIPE_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_PIPE_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_PIPE_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0
|
||||
}
|
||||
if w & DPC_CLR_CMD_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_BUFBUSY_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_BUFBUSY_REG] = 0
|
||||
}
|
||||
|
||||
/* clear clock counter */
|
||||
if w & DPC_CLR_CLOCK_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_CLOCK_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_CLOCK_REG] = 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |=
|
||||
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CBUF_READY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
|
||||
}
|
||||
|
||||
pub fn rdp_interrupt_event(device: &mut device::Device) {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &=
|
||||
!(DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_DP)
|
||||
|
||||
@@ -3,25 +3,25 @@ use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
//const RDRAM_CONFIG_REG: u32 = 0;
|
||||
//const RDRAM_DEVICE_ID_REG: u32 = 1;
|
||||
//const RDRAM_DELAY_REG: u32 = 2;
|
||||
const RDRAM_MODE_REG: u32 = 3;
|
||||
//const RDRAM_REF_INTERVAL_REG: u32 = 4;
|
||||
//const RDRAM_REF_ROW_REG: u32 = 5;
|
||||
//const RDRAM_RAS_INTERVAL_REG: u32 = 6;
|
||||
//const RDRAM_MIN_INTERVAL_REG: u32 = 7;
|
||||
//const RDRAM_ADDR_SELECT_REG: u32 = 8;
|
||||
//const RDRAM_DEVICE_MANUF_REG: u32 = 9;
|
||||
pub const RDRAM_REGS_COUNT: u32 = 10;
|
||||
//const RDRAM_CONFIG_REG: usize = 0;
|
||||
//const RDRAM_DEVICE_ID_REG: usize = 1;
|
||||
//const RDRAM_DELAY_REG: usize = 2;
|
||||
const RDRAM_MODE_REG: usize = 3;
|
||||
//const RDRAM_REF_INTERVAL_REG: usize = 4;
|
||||
//const RDRAM_REF_ROW_REG: usize = 5;
|
||||
//const RDRAM_RAS_INTERVAL_REG: usize = 6;
|
||||
//const RDRAM_MIN_INTERVAL_REG: usize = 7;
|
||||
//const RDRAM_ADDR_SELECT_REG: usize = 8;
|
||||
//const RDRAM_DEVICE_MANUF_REG: usize = 9;
|
||||
pub const RDRAM_REGS_COUNT: usize = 10;
|
||||
|
||||
pub const RDRAM_MASK: usize = 0xFFFFFF;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rdram {
|
||||
pub mem: Vec<u8>,
|
||||
pub size: u32,
|
||||
pub regs: [[u32; RDRAM_REGS_COUNT as usize]; 4],
|
||||
pub regs: [[u32; RDRAM_REGS_COUNT]; 4],
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
@@ -91,9 +91,8 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
|
||||
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
|
||||
}
|
||||
|
||||
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
|
||||
& device::mi::MI_INIT_LENGTH_MASK)
|
||||
+ 1;
|
||||
let repeat_length =
|
||||
(device.mi.regs[device::mi::MI_INIT_MODE_REG] & device::mi::MI_INIT_LENGTH_MASK) + 1;
|
||||
|
||||
if !repeat_length.is_multiple_of(4) {
|
||||
panic!(
|
||||
@@ -113,7 +112,7 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG] &= !device::mi::MI_INIT_MODE;
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
@@ -127,7 +126,7 @@ pub fn read_regs(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let chip_id = (address >> 13) & 3;
|
||||
let reg = (address & 0x3FF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
RDRAM_MODE_REG => device.pi.regs[reg as usize] ^ 0xc0c0c0c0,
|
||||
0x80 => 0x00000000, //Row, needed for libdragon
|
||||
_ => device.rdram.regs[chip_id as usize][reg as usize],
|
||||
@@ -152,7 +151,7 @@ pub fn init(device: &mut device::Device) {
|
||||
device.rdram.mem =
|
||||
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
|
||||
|
||||
retroachievements::set_dmem(device.rdram.mem.as_ptr(), device.rdram.size as usize);
|
||||
retroachievements::set_rdram(device.rdram.mem.as_ptr(), device.rdram.size as usize);
|
||||
|
||||
// hack, skip RDRAM initialization
|
||||
device
|
||||
@@ -169,8 +168,8 @@ pub fn init(device: &mut device::Device) {
|
||||
.unwrap_or(&mut [0; 4])
|
||||
.copy_from_slice(&device.rdram.size.to_ne_bytes());
|
||||
|
||||
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
|
||||
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
|
||||
device.ri.regs[device::ri::RI_MODE_REG] = 0x0e;
|
||||
device.ri.regs[device::ri::RI_CONFIG_REG] = 0x40;
|
||||
}
|
||||
|
||||
pub fn rdram_calculate_cycles(length: u64) -> u64 {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::device;
|
||||
|
||||
pub const RI_MODE_REG: u32 = 0;
|
||||
pub const RI_CONFIG_REG: u32 = 1;
|
||||
//const RI_CURRENT_LOAD_REG: u32 = 2;
|
||||
pub const RI_SELECT_REG: u32 = 3;
|
||||
pub const RI_REFRESH_REG: u32 = 4;
|
||||
//const RI_LATENCY_REG: u32 = 5;
|
||||
//const RI_ERROR_REG: u32 = 6;
|
||||
//const RI_WERROR_REG: u32 = 7;
|
||||
pub const RI_REGS_COUNT: u32 = 8;
|
||||
pub const RI_MODE_REG: usize = 0;
|
||||
pub const RI_CONFIG_REG: usize = 1;
|
||||
//const RI_CURRENT_LOAD_REG: usize = 2;
|
||||
pub const RI_SELECT_REG: usize = 3;
|
||||
pub const RI_REFRESH_REG: usize = 4;
|
||||
//const RI_LATENCY_REG: usize = 5;
|
||||
//const RI_ERROR_REG: usize = 6;
|
||||
//const RI_WERROR_REG: usize = 7;
|
||||
pub const RI_REGS_COUNT: usize = 8;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ri {
|
||||
pub regs: [u32; RI_REGS_COUNT as usize],
|
||||
pub regs: [u32; RI_REGS_COUNT],
|
||||
pub ram_init: bool,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn read_regs(
|
||||
) -> u32 {
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
RI_SELECT_REG => {
|
||||
if !device.ri.ram_init {
|
||||
device::cop0::add_cycles(device, device.cpu.clock_rate / 2); // hack, simulate RDRAM initialization
|
||||
@@ -38,7 +38,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
if reg as u32 == RI_SELECT_REG {
|
||||
if reg as usize == RI_SELECT_REG {
|
||||
device.ri.ram_init = false;
|
||||
}
|
||||
device::memory::masked_write_32(&mut device.ri.regs[reg as usize], value, mask);
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{device, savestates};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BranchState {
|
||||
pub state: device::cpu::State,
|
||||
pub pc: u32,
|
||||
@@ -25,7 +25,7 @@ pub enum InstructionType {
|
||||
Vu,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cpu {
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub instructions: [Instructions; 0x1000 / 4],
|
||||
@@ -132,48 +132,47 @@ pub fn run(device: &mut device::Device) -> u64 {
|
||||
device.rsp.cpu.gpr[0] = 0; // gpr 0 is read only
|
||||
|
||||
let instruction = device.rsp.cpu.instructions
|
||||
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] / 4) as usize];
|
||||
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG] / 4) as usize];
|
||||
(instruction.func)(device, instruction.opcode);
|
||||
|
||||
match device.rsp.cpu.branch_state.state {
|
||||
device::cpu::State::Step => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::Take => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotTaken
|
||||
}
|
||||
device::cpu::State::NotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotNotTaken
|
||||
}
|
||||
device::cpu::State::DelaySlotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] =
|
||||
device.rsp.cpu.branch_state.pc;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] = device.rsp.cpu.branch_state.pc;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::DelaySlotNotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::Discard => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 8;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 8;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step
|
||||
}
|
||||
device::cpu::State::Exception => {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step
|
||||
}
|
||||
}
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] &= 0xFFC;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] &= 0xFFC;
|
||||
|
||||
if device.rsp.cpu.instruction_type == device.rsp.cpu.last_instruction_type {
|
||||
device.rsp.cpu.cycle_counter += 1;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SP_MEM_ADDR_REG: u32 = 0;
|
||||
const SP_DRAM_ADDR_REG: u32 = 1;
|
||||
const SP_RD_LEN_REG: u32 = 2;
|
||||
const SP_WR_LEN_REG: u32 = 3;
|
||||
pub const SP_STATUS_REG: u32 = 4;
|
||||
const SP_DMA_FULL_REG: u32 = 5;
|
||||
const SP_DMA_BUSY_REG: u32 = 6;
|
||||
const SP_SEMAPHORE_REG: u32 = 7;
|
||||
pub const SP_REGS_COUNT: u32 = 8;
|
||||
const SP_MEM_ADDR_REG: usize = 0;
|
||||
const SP_DRAM_ADDR_REG: usize = 1;
|
||||
const SP_RD_LEN_REG: usize = 2;
|
||||
const SP_WR_LEN_REG: usize = 3;
|
||||
pub const SP_STATUS_REG: usize = 4;
|
||||
const SP_DMA_FULL_REG: usize = 5;
|
||||
const SP_DMA_BUSY_REG: usize = 6;
|
||||
const SP_SEMAPHORE_REG: usize = 7;
|
||||
pub const SP_REGS_COUNT: usize = 8;
|
||||
|
||||
pub const SP_PC_REG: u32 = 0;
|
||||
//const SP_IBIST_REG: u32 = 1;
|
||||
pub const SP_REGS2_COUNT: u32 = 2;
|
||||
pub const SP_PC_REG: usize = 0;
|
||||
//const SP_IBIST_REG: usize = 1;
|
||||
pub const SP_REGS2_COUNT: usize = 2;
|
||||
|
||||
/* SP_STATUS - read */
|
||||
pub const SP_STATUS_HALT: u32 = 1 << 0;
|
||||
@@ -76,11 +76,11 @@ pub struct RspDma {
|
||||
pub dramaddr: u32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rsp {
|
||||
pub cpu: device::rsp_cpu::Cpu,
|
||||
pub regs: [u32; SP_REGS_COUNT as usize],
|
||||
pub regs2: [u32; SP_REGS2_COUNT as usize],
|
||||
pub regs: [u32; SP_REGS_COUNT],
|
||||
pub regs2: [u32; SP_REGS2_COUNT],
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub mem: [u8; 0x2000],
|
||||
pub fifo: [RspDma; 2],
|
||||
@@ -204,10 +204,10 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
device.rsp.regs[SP_MEM_ADDR_REG as usize] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
|
||||
device.rsp.regs[SP_DRAM_ADDR_REG as usize] = dram_addr;
|
||||
device.rsp.regs[SP_RD_LEN_REG as usize] = 0xff8;
|
||||
device.rsp.regs[SP_WR_LEN_REG as usize] = 0xff8;
|
||||
device.rsp.regs[SP_MEM_ADDR_REG] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
|
||||
device.rsp.regs[SP_DRAM_ADDR_REG] = dram_addr;
|
||||
device.rsp.regs[SP_RD_LEN_REG] = 0xff8;
|
||||
device.rsp.regs[SP_WR_LEN_REG] = 0xff8;
|
||||
|
||||
device::events::create_event(
|
||||
device,
|
||||
@@ -217,52 +217,52 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
}
|
||||
|
||||
fn fifo_push(device: &mut device::Device, dir: DmaDir) {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
|
||||
panic!("RSP DMA already full")
|
||||
}
|
||||
|
||||
device.rsp.cpu.sync_point = true;
|
||||
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG] != 0 {
|
||||
device.rsp.fifo[1].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG as usize]
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG]
|
||||
} else {
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG as usize]
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG]
|
||||
}
|
||||
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
|
||||
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
|
||||
device.rsp.regs[SP_DMA_FULL_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_FULL
|
||||
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
|
||||
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
|
||||
device.rsp.regs[SP_DMA_FULL_REG] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_FULL
|
||||
} else {
|
||||
device.rsp.fifo[0].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG as usize]
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG]
|
||||
} else {
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG as usize]
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG]
|
||||
}
|
||||
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
|
||||
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_BUSY;
|
||||
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
|
||||
device.rsp.regs[SP_DMA_BUSY_REG] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_BUSY;
|
||||
|
||||
do_dma(device, device.rsp.fifo[0])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fifo_pop(device: &mut device::Device) {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
|
||||
device.rsp.fifo[0].dir = device.rsp.fifo[1].dir;
|
||||
device.rsp.fifo[0].length = device.rsp.fifo[1].length;
|
||||
device.rsp.fifo[0].memaddr = device.rsp.fifo[1].memaddr;
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.fifo[1].dramaddr;
|
||||
device.rsp.regs[SP_DMA_FULL_REG as usize] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_FULL;
|
||||
device.rsp.regs[SP_DMA_FULL_REG] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_FULL;
|
||||
|
||||
do_dma(device, device.rsp.fifo[0])
|
||||
} else {
|
||||
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_BUSY;
|
||||
device.rsp.regs[SP_DMA_BUSY_REG] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_BUSY;
|
||||
if device.rsp.run_after_dma {
|
||||
device.rsp.run_after_dma = false;
|
||||
do_task(device);
|
||||
@@ -279,7 +279,7 @@ pub fn read_regs(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_STATUS_REG => {
|
||||
let value = device.rsp.regs[reg as usize]
|
||||
& (SP_STATUS_SIG0
|
||||
@@ -311,7 +311,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_STATUS_REG => update_sp_status(device, value),
|
||||
SP_RD_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.rsp.regs[reg as usize], value, mask);
|
||||
@@ -339,7 +339,7 @@ pub fn read_regs2(
|
||||
|
||||
pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_PC_REG => {
|
||||
device::memory::masked_write_32(
|
||||
&mut device.rsp.regs2[reg as usize],
|
||||
@@ -352,20 +352,20 @@ pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask:
|
||||
}
|
||||
|
||||
fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
let was_halted = device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT != 0;
|
||||
let was_halted = device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT != 0;
|
||||
|
||||
/* clear / set halt */
|
||||
if w & SP_CLR_HALT != 0 && w & SP_SET_HALT == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_HALT
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_HALT
|
||||
}
|
||||
if w & SP_SET_HALT != 0 && w & SP_CLR_HALT == 0 {
|
||||
device::events::remove_event(device, device::events::EVENT_TYPE_SP);
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT
|
||||
}
|
||||
|
||||
/* clear broke */
|
||||
if w & SP_CLR_BROKE != 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_BROKE
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_BROKE
|
||||
}
|
||||
|
||||
/* clear SP interrupt */
|
||||
@@ -379,85 +379,85 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
|
||||
/* clear / set single step */
|
||||
if w & SP_CLR_SSTEP != 0 && w & SP_SET_SSTEP == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SSTEP
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SSTEP
|
||||
}
|
||||
if w & SP_SET_SSTEP != 0 && w & SP_CLR_SSTEP == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SSTEP
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SSTEP
|
||||
}
|
||||
|
||||
/* clear / set interrupt on break */
|
||||
if w & SP_CLR_INTR_BREAK != 0 && w & SP_SET_INTR_BREAK == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_INTR_BREAK
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_INTR_BREAK
|
||||
}
|
||||
if w & SP_SET_INTR_BREAK != 0 && w & SP_CLR_INTR_BREAK == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_INTR_BREAK
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_INTR_BREAK
|
||||
}
|
||||
|
||||
/* clear / set signal 0 */
|
||||
if w & SP_CLR_SIG0 != 0 && w & SP_SET_SIG0 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG0
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG0
|
||||
}
|
||||
if w & SP_SET_SIG0 != 0 && w & SP_CLR_SIG0 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG0
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG0
|
||||
}
|
||||
|
||||
/* clear / set signal 1 */
|
||||
if w & SP_CLR_SIG1 != 0 && w & SP_SET_SIG1 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG1
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG1
|
||||
}
|
||||
if w & SP_SET_SIG1 != 0 && w & SP_CLR_SIG1 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG1
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG1
|
||||
}
|
||||
|
||||
/* clear / set signal 2 */
|
||||
if w & SP_CLR_SIG2 != 0 && w & SP_SET_SIG2 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG2
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG2
|
||||
}
|
||||
if w & SP_SET_SIG2 != 0 && w & SP_CLR_SIG2 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG2
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG2
|
||||
}
|
||||
|
||||
/* clear / set signal 3 */
|
||||
if w & SP_CLR_SIG3 != 0 && w & SP_SET_SIG3 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG3
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG3
|
||||
}
|
||||
if w & SP_SET_SIG3 != 0 && w & SP_CLR_SIG3 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG3
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG3
|
||||
}
|
||||
|
||||
/* clear / set signal 4 */
|
||||
if w & SP_CLR_SIG4 != 0 && w & SP_SET_SIG4 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG4
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG4
|
||||
}
|
||||
if w & SP_SET_SIG4 != 0 && w & SP_CLR_SIG4 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG4
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG4
|
||||
}
|
||||
|
||||
/* clear / set signal 5 */
|
||||
if w & SP_CLR_SIG5 != 0 && w & SP_SET_SIG5 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG5
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG5
|
||||
}
|
||||
if w & SP_SET_SIG5 != 0 && w & SP_CLR_SIG5 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG5
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG5
|
||||
}
|
||||
|
||||
/* clear / set signal 6 */
|
||||
if w & SP_CLR_SIG6 != 0 && w & SP_SET_SIG6 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG6
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG6
|
||||
}
|
||||
if w & SP_SET_SIG6 != 0 && w & SP_CLR_SIG6 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG6
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG6
|
||||
}
|
||||
|
||||
/* clear / set signal 7 */
|
||||
if w & SP_CLR_SIG7 != 0 && w & SP_SET_SIG7 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG7
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG7
|
||||
}
|
||||
if w & SP_SET_SIG7 != 0 && w & SP_CLR_SIG7 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG7
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG7
|
||||
}
|
||||
|
||||
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT == 0 && was_halted {
|
||||
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT == 0 && was_halted {
|
||||
device.rsp.cpu.broken = false;
|
||||
device.rsp.cpu.halted = false;
|
||||
do_task(device);
|
||||
@@ -468,7 +468,7 @@ fn do_task(device: &mut device::Device) {
|
||||
device.rsp.cpu.sync_point = false;
|
||||
device.rsp.last_status_value = 0;
|
||||
device.rdp.last_status_value = 0;
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG] == 1 {
|
||||
device.rsp.run_after_dma = true
|
||||
} else {
|
||||
let timer = device::rsp_cpu::run(device);
|
||||
@@ -479,21 +479,21 @@ fn do_task(device: &mut device::Device) {
|
||||
|
||||
pub fn rsp_event(device: &mut device::Device) {
|
||||
if device.rsp.cpu.broken {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT | SP_STATUS_BROKE;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT | SP_STATUS_BROKE;
|
||||
|
||||
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_INTR_BREAK != 0 {
|
||||
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_INTR_BREAK != 0 {
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SP)
|
||||
}
|
||||
return;
|
||||
}
|
||||
if device.rsp.cpu.halted {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT;
|
||||
return;
|
||||
}
|
||||
do_task(device)
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] = 1;
|
||||
device::rsp_cpu::init(device);
|
||||
}
|
||||
|
||||
@@ -183,23 +183,22 @@ pub fn j(device: &mut device::Device, opcode: u32) {
|
||||
return;
|
||||
}
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
|
||||
& 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
}
|
||||
|
||||
pub fn jal(device: &mut device::Device, opcode: u32) {
|
||||
if device::rsp_cpu::in_delay_slot_taken(device) {
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
|
||||
} else {
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
if !device::rsp_cpu::in_delay_slot_taken(device) {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
|
||||
& 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
} else if !device::rsp_cpu::in_delay_slot(device) {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
@@ -208,8 +207,7 @@ pub fn jal(device: &mut device::Device, opcode: u32) {
|
||||
pub fn beq(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] == device.rsp.cpu.gpr[rt(opcode) as usize] {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -220,8 +218,7 @@ pub fn beq(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bne(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] != device.rsp.cpu.gpr[rt(opcode) as usize] {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -232,8 +229,7 @@ pub fn bne(device: &mut device::Device, opcode: u32) {
|
||||
pub fn blez(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 <= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -244,8 +240,7 @@ pub fn blez(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bgtz(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 > 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -485,7 +480,7 @@ pub fn jalr(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.gpr[rd(opcode) as usize] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
|
||||
} else {
|
||||
device.rsp.cpu.gpr[rd(opcode) as usize] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,8 +542,7 @@ pub fn sltu(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bltz(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -559,8 +553,7 @@ pub fn bltz(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bgez(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 >= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -571,34 +564,30 @@ pub fn bgez(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bltzal(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
|
||||
pub fn bgezal(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) >= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
|
||||
pub fn mfc0(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.cycle_counter += 2;
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rsp_interface::read_regs(
|
||||
device,
|
||||
(rd(opcode) << 2) as u64,
|
||||
@@ -607,14 +596,14 @@ pub fn mfc0(device: &mut device::Device, opcode: u32) {
|
||||
} else {
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rdp::read_regs_dpc(
|
||||
device,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
|
||||
device::memory::AccessSize::Word,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mtc0(device: &mut device::Device, opcode: u32) {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
|
||||
device::rsp_interface::write_regs(
|
||||
device,
|
||||
(rd(opcode) << 2) as u64,
|
||||
@@ -624,15 +613,15 @@ pub fn mtc0(device: &mut device::Device, opcode: u32) {
|
||||
} else {
|
||||
device::rdp::write_regs_dpc(
|
||||
device,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize],
|
||||
0xFFFFFFFF,
|
||||
)
|
||||
}
|
||||
if rd(opcode) == device::rsp_interface::SP_STATUS_REG
|
||||
if rd(opcode) as usize == device::rsp_interface::SP_STATUS_REG
|
||||
&& device.rsp.cpu.gpr[rt(opcode) as usize] & device::rsp_interface::SP_SET_HALT != 0
|
||||
{
|
||||
device.rsp.regs[device::rsp_interface::SP_STATUS_REG as usize] &=
|
||||
device.rsp.regs[device::rsp_interface::SP_STATUS_REG] &=
|
||||
!device::rsp_interface::SP_STATUS_HALT; // set halt when event happens
|
||||
device.rsp.cpu.halted = true // the RSP can halt itself by setting SP_SET_HALT
|
||||
}
|
||||
|
||||
@@ -2,30 +2,30 @@ use crate::device;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const SI_DRAM_ADDR_REG: u32 = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
|
||||
//const SI_R2_REG: u32 = 2;
|
||||
//const SI_R3_REG: u32 = 3;
|
||||
const SI_PIF_ADDR_WR64B_REG: u32 = 4;
|
||||
//const SI_R5_REG: u32 = 5;
|
||||
pub const SI_STATUS_REG: u32 = 6;
|
||||
pub const SI_REGS_COUNT: u32 = 7;
|
||||
const SI_DRAM_ADDR_REG: usize = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: usize = 1;
|
||||
//const SI_R2_REG: usize = 2;
|
||||
//const SI_R3_REG: usize = 3;
|
||||
const SI_PIF_ADDR_WR64B_REG: usize = 4;
|
||||
//const SI_R5_REG: usize = 5;
|
||||
pub const SI_STATUS_REG: usize = 6;
|
||||
pub const SI_REGS_COUNT: usize = 7;
|
||||
|
||||
pub const SI_STATUS_DMA_BUSY: u32 = 1 << 0;
|
||||
pub const SI_STATUS_IO_BUSY: u32 = 1 << 1;
|
||||
//const SI_STATUS_DMA_ERROR: u32 = 1 << 3;
|
||||
const SI_STATUS_INTERRUPT: u32 = 1 << 12;
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DmaDir {
|
||||
None,
|
||||
Write,
|
||||
Read,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Si {
|
||||
pub regs: [u32; SI_REGS_COUNT as usize],
|
||||
pub regs: [u32; SI_REGS_COUNT],
|
||||
pub dma_dir: DmaDir,
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ fn dma_read(device: &mut device::Device) {
|
||||
|
||||
let duration = device::pif::update_pif_ram(device);
|
||||
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
|
||||
|
||||
let length = duration + randomize_interrupt_time(&mut device.rng);
|
||||
|
||||
@@ -59,7 +59,7 @@ fn dma_write(device: &mut device::Device) {
|
||||
|
||||
copy_pif_rdram(device);
|
||||
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
|
||||
|
||||
let length = 6000 + randomize_interrupt_time(&mut device.rng); //based on https://github.com/rasky/n64-systembench
|
||||
|
||||
@@ -68,7 +68,7 @@ fn dma_write(device: &mut device::Device) {
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SI_STATUS_REG => {
|
||||
device.si.regs[reg as usize] &= !SI_STATUS_INTERRUPT;
|
||||
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_SI)
|
||||
@@ -81,7 +81,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
|
||||
//rdram is in native endian format, and pif memory is in big endian format
|
||||
fn copy_pif_rdram(device: &mut device::Device) {
|
||||
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG as usize] as usize & device::rdram::RDRAM_MASK;
|
||||
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG] as usize & device::rdram::RDRAM_MASK;
|
||||
if device.si.dma_dir == DmaDir::Write {
|
||||
let mut i = 0;
|
||||
while i < device::pif::PIF_RAM_SIZE {
|
||||
@@ -125,8 +125,8 @@ pub fn dma_event(device: &mut device::Device) {
|
||||
panic!("si dma unknown")
|
||||
}
|
||||
device.si.dma_dir = DmaDir::None;
|
||||
device.si.regs[SI_STATUS_REG as usize] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_INTERRUPT;
|
||||
device.si.regs[SI_STATUS_REG] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_INTERRUPT;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SI)
|
||||
}
|
||||
|
||||
@@ -34,22 +34,22 @@ pub fn read(device: &mut device::Device, index: u64) {
|
||||
if index > 31 {
|
||||
return;
|
||||
}
|
||||
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] =
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask << 13;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] =
|
||||
((device.cpu.cop0.tlb_entries[index as usize].region as u64) << 62)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].vpn2 << 13)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].asid) as u64;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] =
|
||||
(device.cpu.cop0.tlb_entries[index as usize].pfn_even << 6)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].c_even << 3) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].d_even << 2) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].v_even << 1) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].g) as u64;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] =
|
||||
(device.cpu.cop0.tlb_entries[index as usize].pfn_odd << 6)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].c_odd << 3) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].d_odd << 2) as u64
|
||||
@@ -64,37 +64,37 @@ pub fn write(device: &mut device::Device, index: u64) {
|
||||
tlb_unmap(device, index);
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].g = (device.cpu.cop0.regs
|
||||
[device::cop0::COP0_ENTRYLO0_REG as usize]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize]
|
||||
[device::cop0::COP0_ENTRYLO0_REG]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG]
|
||||
& 1) as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].pfn_even =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 6) & 0xFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 6) & 0xFFFFF;
|
||||
device.cpu.cop0.tlb_entries[index as usize].pfn_odd =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 6) & 0xFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 6) & 0xFFFFF;
|
||||
device.cpu.cop0.tlb_entries[index as usize].c_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 3) & 7) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 3) & 7) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].c_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 3) & 7) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 3) & 7) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].d_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 2) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 2) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].d_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 2) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 2) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].v_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 1) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 1) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].v_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 1) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 1) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].asid =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] >> 13) & 0xFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] >> 13) & 0xFFF;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].vpn2 =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].region =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask &= 0b101010101010;
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask |=
|
||||
@@ -125,22 +125,20 @@ pub fn write(device: &mut device::Device, index: u64) {
|
||||
}
|
||||
|
||||
pub fn probe(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = 0x80000000; // set probe failure
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = 0x80000000; // set probe failure
|
||||
for (pos, e) in device.cpu.cop0.tlb_entries.iter().enumerate() {
|
||||
if e.vpn2 & !e.mask
|
||||
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF)
|
||||
& !e.mask
|
||||
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF) & !e.mask
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8 {
|
||||
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8 {
|
||||
continue;
|
||||
}
|
||||
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8
|
||||
{
|
||||
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8 {
|
||||
continue;
|
||||
}
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = pos as u64;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = pos as u64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -151,15 +149,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
if e.v_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -168,15 +164,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
if e.v_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -193,17 +187,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
|
||||
{
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_even != 2;
|
||||
device.cpu.cop0.tlb_lut_r.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
|
||||
cached: e.c_even != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_even != 2;
|
||||
device.cpu.cop0.tlb_lut_w.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
|
||||
cached: e.c_even != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -216,17 +218,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
|
||||
{
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_odd != 2;
|
||||
device.cpu.cop0.tlb_lut_r.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
|
||||
cached: e.c_odd != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_odd != 2;
|
||||
device.cpu.cop0.tlb_lut_w.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
|
||||
cached: e.c_odd != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -241,19 +251,17 @@ pub fn get_physical_address(
|
||||
address &= 0xffffffff;
|
||||
|
||||
if access_type == device::memory::AccessType::Write {
|
||||
if device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address != 0 {
|
||||
if let Some(lut) = device.cpu.cop0.tlb_lut_w.get(&(address >> 12)) {
|
||||
return (
|
||||
(device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address & 0x1FFFF000)
|
||||
| (address & 0xFFF),
|
||||
device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].cached,
|
||||
(lut.address & 0x1FFFF000) | (address & 0xFFF),
|
||||
lut.cached,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else if device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address != 0 {
|
||||
} else if let Some(lut) = device.cpu.cop0.tlb_lut_r.get(&(address >> 12)) {
|
||||
return (
|
||||
(device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address & 0x1FFFF000)
|
||||
| (address & 0xFFF),
|
||||
device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].cached,
|
||||
(lut.address & 0x1FFFF000) | (address & 0xFFF),
|
||||
lut.cached,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,46 @@
|
||||
use crate::{cheats, device, netplay, retroachievements, ui};
|
||||
use crate::{cheats, device, netplay, retroachievements, savestates, ui};
|
||||
|
||||
const VI_STATUS_REG: u32 = 0;
|
||||
const VI_ORIGIN_REG: u32 = 1;
|
||||
//const VI_WIDTH_REG: u32 = 2;
|
||||
//const VI_V_INTR_REG: u32 = 3;
|
||||
const VI_CURRENT_REG: u32 = 4;
|
||||
//const VI_BURST_REG: u32 = 5;
|
||||
const VI_V_SYNC_REG: u32 = 6;
|
||||
const VI_H_SYNC_REG: u32 = 7;
|
||||
//const VI_LEAP_REG: u32 = 8;
|
||||
//const VI_H_START_REG: u32 = 9;
|
||||
//const VI_V_START_REG: u32 = 10;
|
||||
//const VI_V_BURST_REG: u32 = 11;
|
||||
//const VI_X_SCALE_REG: u32 = 12;
|
||||
//const VI_Y_SCALE_REG: u32 = 13;
|
||||
pub const VI_REGS_COUNT: u32 = 14;
|
||||
const VI_STATUS_REG: usize = 0;
|
||||
const VI_ORIGIN_REG: usize = 1;
|
||||
//const VI_WIDTH_REG: usize = 2;
|
||||
const VI_V_INTR_REG: usize = 3;
|
||||
const VI_CURRENT_REG: usize = 4;
|
||||
//const VI_BURST_REG: usize = 5;
|
||||
const VI_V_SYNC_REG: usize = 6;
|
||||
const VI_H_SYNC_REG: usize = 7;
|
||||
//const VI_LEAP_REG: usize = 8;
|
||||
//const VI_H_START_REG: usize = 9;
|
||||
const VI_V_START_REG: usize = 10;
|
||||
//const VI_V_BURST_REG: usize = 11;
|
||||
//const VI_X_SCALE_REG: usize = 12;
|
||||
//const VI_Y_SCALE_REG: usize = 13;
|
||||
pub const VI_REGS_COUNT: usize = 14;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vi {
|
||||
pub regs: [u32; VI_REGS_COUNT as usize],
|
||||
pub regs: [u32; VI_REGS_COUNT],
|
||||
pub clock: u64,
|
||||
pub delay: u64,
|
||||
pub field: u32,
|
||||
#[serde(skip)]
|
||||
pub next_pace_deadline: Option<std::time::Instant>,
|
||||
pub count_per_scanline: u64,
|
||||
pub enable_speed_limiter: bool,
|
||||
pub vi_counter: u64,
|
||||
pub min_wait_time: std::time::Duration,
|
||||
pub frame_time: f64,
|
||||
pub limit_freq: u64,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "std::time::Instant::now")]
|
||||
pub limit_freq_check: std::time::Instant,
|
||||
pub elapsed_time: f64,
|
||||
}
|
||||
|
||||
const MAX_LIMIT_FREQ: u64 = 3;
|
||||
|
||||
pub fn set_expected_refresh_rate(device: &mut device::Device) {
|
||||
let expected_refresh_rate = device.vi.clock as f64
|
||||
/ (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as f64
|
||||
/ ((device.vi.regs[VI_H_SYNC_REG as usize] & 0xFFF) + 1) as f64
|
||||
/ (device.vi.regs[VI_V_SYNC_REG] + 1) as f64
|
||||
/ ((device.vi.regs[VI_H_SYNC_REG] & 0xFFF) + 1) as f64
|
||||
* 2.0;
|
||||
device.vi.frame_time = 1.0 / expected_refresh_rate;
|
||||
device.vi.delay = (device.cpu.clock_rate as f64 / expected_refresh_rate) as u64;
|
||||
device.vi.count_per_scanline =
|
||||
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
|
||||
|
||||
reset_pace_deadline(device);
|
||||
device.vi.count_per_scanline = device.vi.delay / (device.vi.regs[VI_V_SYNC_REG] + 1) as u64;
|
||||
}
|
||||
|
||||
fn reset_pace_deadline(device: &mut device::Device) {
|
||||
device.vi.next_pace_deadline = None;
|
||||
device.speed_limiter.next_pace_deadline = None;
|
||||
}
|
||||
|
||||
fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
@@ -62,19 +51,19 @@ fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
|
||||
fn set_current_line(device: &mut device::Device) {
|
||||
if let Some(next_vi) = device::events::get_event(device, device::events::EVENT_TYPE_VI) {
|
||||
device.vi.regs[VI_CURRENT_REG as usize] = ((device.vi.delay.saturating_sub(
|
||||
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize],
|
||||
)) / device.vi.count_per_scanline) as u32;
|
||||
device.vi.regs[VI_CURRENT_REG] =
|
||||
((device.vi.delay.saturating_sub(
|
||||
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG],
|
||||
)) / device.vi.count_per_scanline) as u32;
|
||||
|
||||
// wrap around VI_CURRENT_REG if needed
|
||||
if device.vi.regs[VI_CURRENT_REG as usize] >= device.vi.regs[VI_V_SYNC_REG as usize] {
|
||||
device.vi.regs[VI_CURRENT_REG as usize] -= device.vi.regs[VI_V_SYNC_REG as usize]
|
||||
if device.vi.regs[VI_CURRENT_REG] >= device.vi.regs[VI_V_SYNC_REG] {
|
||||
device.vi.regs[VI_CURRENT_REG] -= device.vi.regs[VI_V_SYNC_REG]
|
||||
}
|
||||
}
|
||||
/* update current field */
|
||||
device.vi.regs[VI_CURRENT_REG as usize] =
|
||||
(device.vi.regs[VI_CURRENT_REG as usize] & !1) | device.vi.field;
|
||||
ui::video::set_register(VI_CURRENT_REG, device.vi.regs[VI_CURRENT_REG as usize])
|
||||
device.vi.regs[VI_CURRENT_REG] = (device.vi.regs[VI_CURRENT_REG] & !1) | device.vi.field;
|
||||
ui::video::set_register(VI_CURRENT_REG as u32, device.vi.regs[VI_CURRENT_REG])
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
@@ -83,7 +72,7 @@ pub fn read_regs(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
if reg as u32 == VI_CURRENT_REG {
|
||||
if reg as usize == VI_CURRENT_REG {
|
||||
set_current_line(device)
|
||||
}
|
||||
device::cop0::add_cycles(device, 20);
|
||||
@@ -92,7 +81,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
VI_CURRENT_REG => device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_VI),
|
||||
VI_V_SYNC_REG => {
|
||||
if device.vi.regs[reg as usize] != value & mask {
|
||||
@@ -111,7 +100,12 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
let current_origin = device.vi.regs[reg as usize];
|
||||
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
|
||||
if current_origin != device.vi.regs[reg as usize] {
|
||||
let _ = device.ui.video.fps_tx.try_send(true);
|
||||
if device.netplay.is_none() {
|
||||
savestates::process_savestates(device);
|
||||
}
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -121,45 +115,58 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
ui::video::set_register(reg as u32, device.vi.regs[reg as usize])
|
||||
}
|
||||
|
||||
pub fn update_screen(device: &mut device::Device) {
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
ui::video::render_frame();
|
||||
}
|
||||
|
||||
let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
||||
|
||||
if speed_limiter_toggled {
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
|
||||
if !netplay::in_rollback(device.netplay.as_ref())
|
||||
&& device
|
||||
.speed_limiter
|
||||
.frame_counter
|
||||
.is_multiple_of(device.speed_limiter.limit_freq)
|
||||
&& device.speed_limiter.enabled
|
||||
{
|
||||
speed_limiter(device);
|
||||
}
|
||||
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
ui::video::update_screen();
|
||||
device.speed_limiter.frame_counter += 1;
|
||||
let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
|
||||
}
|
||||
|
||||
if device.netplay.is_some() {
|
||||
device.netplay.as_mut().unwrap().inputs = netplay::process_requests(device);
|
||||
} else if paused {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message(
|
||||
"Cannot pause in RA hardcore mode",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::pause_loop(&mut device.ui, device.vi.frame_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
device.vi.elapsed_time += device.vi.frame_time;
|
||||
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
|
||||
ui::video::render_frame();
|
||||
let _ = device.ui.video.vis_tx.try_send(true);
|
||||
|
||||
retroachievements::do_frame();
|
||||
|
||||
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::send_sync_check(netplay, device.cpu.cop0.regs.as_ref());
|
||||
if device.vi.enable_speed_limiter == netplay.fast_forward {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = !netplay.fast_forward;
|
||||
}
|
||||
}
|
||||
|
||||
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
|
||||
speed_limiter(device, speed_limiter_toggled);
|
||||
}
|
||||
|
||||
ui::video::update_screen();
|
||||
device.vi.vi_counter += 1;
|
||||
|
||||
if device.netplay.is_none() && paused {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message("Cannot pause in RA hardcore mode", false);
|
||||
} else {
|
||||
ui::video::pause_loop(device.vi.frame_time);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
|
||||
/* toggle vi field if in interlaced mode */
|
||||
device.vi.field ^= (device.vi.regs[VI_STATUS_REG as usize] >> 6) & 0x1;
|
||||
device.vi.field ^= (device.vi.regs[VI_STATUS_REG] >> 6) & 0x1;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_VI);
|
||||
|
||||
@@ -167,7 +174,17 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
device,
|
||||
device::events::EVENT_TYPE_VI,
|
||||
device.cpu.next_event_count + device.vi.delay,
|
||||
)
|
||||
);
|
||||
|
||||
let v_intr = device.vi.regs[VI_V_INTR_REG] & 0x3FF;
|
||||
let v_start = (device.vi.regs[VI_V_START_REG] >> 16) & 0x3FF;
|
||||
if v_start > v_intr {
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_UPDATE_SCREEN,
|
||||
(v_start - v_intr) as u64 * device.vi.count_per_scanline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
@@ -178,56 +195,68 @@ pub fn init(device: &mut device::Device) {
|
||||
}
|
||||
}
|
||||
|
||||
fn speed_limiter(device: &mut device::Device, mut speed_limiter_toggled: bool) {
|
||||
let interval =
|
||||
std::time::Duration::from_secs_f64(device.vi.frame_time * device.vi.limit_freq as f64);
|
||||
fn speed_limiter(device: &mut device::Device) {
|
||||
let mut speed_limiter_toggled = false;
|
||||
let mut interval = std::time::Duration::from_secs_f64(
|
||||
device.vi.frame_time * device.speed_limiter.limit_freq as f64,
|
||||
);
|
||||
|
||||
if let Some(netplay) = &device.netplay {
|
||||
let ahead = netplay.session.frames_ahead();
|
||||
if ahead > 0 {
|
||||
interval = interval.mul_f64(1.0 + 0.05 * ahead.min(2) as f64);
|
||||
} else if ahead < 0 {
|
||||
interval = interval.mul_f64(1.0 - 0.05 * (-ahead).min(2) as f64);
|
||||
}
|
||||
}
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
match device.vi.next_pace_deadline {
|
||||
match device.speed_limiter.next_pace_deadline {
|
||||
None => {
|
||||
device.vi.next_pace_deadline = Some(now + interval);
|
||||
device.speed_limiter.next_pace_deadline = Some(now + interval);
|
||||
speed_limiter_toggled = true;
|
||||
}
|
||||
Some(deadline) => {
|
||||
if now < deadline {
|
||||
let dur = deadline - now;
|
||||
spin_sleep::sleep(dur);
|
||||
if dur < device.vi.min_wait_time {
|
||||
device.vi.min_wait_time = dur;
|
||||
|
||||
if dur < device.speed_limiter.min_wait_time {
|
||||
device.speed_limiter.min_wait_time = dur;
|
||||
}
|
||||
} else {
|
||||
//println!("did not sleep");
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(0);
|
||||
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(0);
|
||||
}
|
||||
let mut next = deadline + interval;
|
||||
let t = std::time::Instant::now();
|
||||
while next <= t {
|
||||
next += interval;
|
||||
}
|
||||
device.vi.next_pace_deadline = Some(next);
|
||||
device.speed_limiter.next_pace_deadline = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
|
||||
if std::time::Instant::now().duration_since(device.speed_limiter.limit_freq_check)
|
||||
> std::time::Duration::from_secs(1)
|
||||
{
|
||||
if !speed_limiter_toggled {
|
||||
if device.vi.min_wait_time == std::time::Duration::from_secs(0)
|
||||
&& device.vi.limit_freq < MAX_LIMIT_FREQ
|
||||
if device.speed_limiter.min_wait_time == std::time::Duration::from_secs(0)
|
||||
&& device.speed_limiter.limit_freq < MAX_LIMIT_FREQ
|
||||
{
|
||||
device.vi.limit_freq += 1;
|
||||
device.speed_limiter.limit_freq += 1;
|
||||
reset_pace_deadline(device);
|
||||
} else if device.vi.min_wait_time
|
||||
} else if device.speed_limiter.min_wait_time
|
||||
> std::time::Duration::from_secs_f64(device.vi.frame_time)
|
||||
&& device.vi.limit_freq > 1
|
||||
&& device.speed_limiter.limit_freq > 1
|
||||
{
|
||||
device.vi.limit_freq -= 1;
|
||||
device.speed_limiter.limit_freq -= 1;
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
}
|
||||
|
||||
//println!("limit freq: {}", device.vi.limit_freq);
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(1);
|
||||
device.vi.limit_freq_check = std::time::Instant::now();
|
||||
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(1);
|
||||
device.speed_limiter.limit_freq_check = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
#![deny(warnings)]
|
||||
|
||||
mod cheats;
|
||||
mod device;
|
||||
mod netplay;
|
||||
mod retroachievements;
|
||||
mod savestates;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
#[cfg(target_os = "android")]
|
||||
use slint::ComponentHandle;
|
||||
use std::io::Error;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use ui::android;
|
||||
|
||||
/// N64 emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
|
||||
pub struct Args {
|
||||
pub game: Option<String>,
|
||||
#[arg(short, long)]
|
||||
pub fullscreen: bool,
|
||||
#[arg(long)]
|
||||
pub widescreen: bool,
|
||||
#[arg(long)]
|
||||
pub overclock: Option<bool>,
|
||||
#[arg(long)]
|
||||
pub disable_expansion_pak: Option<bool>,
|
||||
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
|
||||
pub cheats: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_SERVER_ADDR", hide = true)]
|
||||
pub netplay_server_addr: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
|
||||
pub netplay_player_number: Option<usize>,
|
||||
#[arg(long, value_name = "NETPLAY_NUMBER_OF_PLAYERS", hide = true)]
|
||||
pub netplay_number_of_players: Option<usize>,
|
||||
#[arg(long, value_name = "NETPLAY_INPUT_DELAY", hide = true)]
|
||||
pub netplay_input_delay: Option<usize>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)"
|
||||
)]
|
||||
pub configure_input_profile: Option<String>,
|
||||
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
|
||||
pub use_dinput: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DEADZONE_PERCENTAGE",
|
||||
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
|
||||
)]
|
||||
pub deadzone: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Must also specify --port. Used to bind a previously created profile to a port"
|
||||
)]
|
||||
pub bind_input_profile: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Lists connected controllers which can be used in --assign-controller"
|
||||
)]
|
||||
pub list_controllers: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "CONTROLLER_NUMBER",
|
||||
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
|
||||
)]
|
||||
pub assign_controller: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PORT",
|
||||
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
|
||||
)]
|
||||
pub port: Option<usize>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Clear all input profile bindings and controller assignments"
|
||||
)]
|
||||
pub clear_input_bindings: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "SLOT",
|
||||
help = "Load savestate from slot 0-9 when starting the game"
|
||||
)]
|
||||
pub load_state: Option<u32>,
|
||||
#[arg(
|
||||
long = "ra-username",
|
||||
value_name = "USERNAME",
|
||||
help = "Username for RetroAchievements"
|
||||
)]
|
||||
pub ra_username: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-password",
|
||||
value_name = "PASSWORD",
|
||||
help = "Password for RetroAchievements"
|
||||
)]
|
||||
pub ra_password: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
fn set_app_id() {
|
||||
if let Err(e) = slint::set_xdg_app_id(ui::APP_ID) {
|
||||
eprintln!("Could not set application ID: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_runtime() -> (tokio::sync::oneshot::Sender<()>, tokio::runtime::Handle) {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (close_tx, close_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to build runtime");
|
||||
tx.send(rt.handle().clone()).unwrap();
|
||||
rt.block_on(close_rx).unwrap();
|
||||
});
|
||||
|
||||
(close_tx, rx.blocking_recv().unwrap())
|
||||
}
|
||||
|
||||
pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
std::fs::create_dir_all(&dirs.config_dir)?;
|
||||
std::fs::create_dir_all(&dirs.cache_dir)?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
|
||||
|
||||
ui::sdl_hints();
|
||||
|
||||
if let Some(game) = args.game {
|
||||
let file_path = std::path::Path::new(&game).to_path_buf();
|
||||
let Some(rom_contents) = device::get_rom_contents(&file_path) else {
|
||||
return Err(Error::other(format!(
|
||||
"Could not read ROM file: {}",
|
||||
file_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
let cheats = if let Some(cheats_file) = args.cheats {
|
||||
if let Ok(data) = std::fs::read(cheats_file)
|
||||
&& let Ok(cheats) = serde_json::from_slice(&data)
|
||||
{
|
||||
cheats
|
||||
} else {
|
||||
return Err(Error::other("Could not read cheats file"));
|
||||
}
|
||||
} else {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
ui::config::Cheats::new()
|
||||
.cheats
|
||||
.get(&game_crc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
if let Some(slot) = args.load_state
|
||||
&& slot > 9
|
||||
{
|
||||
return Err(Error::other("Savestate slot must be between 0 and 9"));
|
||||
}
|
||||
|
||||
let mut device = device::Device::new(true);
|
||||
|
||||
device.ui.config.ui.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.ui.recent_roms.insert(0, game);
|
||||
device.ui.config.ui.recent_roms.truncate(5);
|
||||
|
||||
if args.fullscreen {
|
||||
device.ui.video.fullscreen = true;
|
||||
} else {
|
||||
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
|
||||
}
|
||||
|
||||
if args.widescreen {
|
||||
device.ui.video.widescreen = true;
|
||||
} else {
|
||||
device.ui.video.widescreen = device.ui.config.video.widescreen;
|
||||
}
|
||||
|
||||
let overclock = args
|
||||
.overclock
|
||||
.unwrap_or(device.ui.config.emulation.overclock);
|
||||
let disable_expansion_pak = args
|
||||
.disable_expansion_pak
|
||||
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
|
||||
|
||||
let mut shutdown_tx = None;
|
||||
let mut usb_handle = None;
|
||||
|
||||
if args.netplay_server_addr.is_none() {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_rom_path[i].is_empty()
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
|
||||
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
|
||||
{
|
||||
device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram);
|
||||
}
|
||||
}
|
||||
|
||||
if device.ui.config.emulation.usb {
|
||||
(shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init();
|
||||
}
|
||||
}
|
||||
|
||||
let file_path = dirs.config_dir.join("retroachievements.json");
|
||||
let ra_config = if let ra_config = std::fs::read(&file_path).unwrap_or_default()
|
||||
&& let ra_config =
|
||||
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
|
||||
.unwrap_or_default()
|
||||
&& (ra_config.enabled || args.ra_username.is_some())
|
||||
{
|
||||
let username = args.ra_username.unwrap_or(ra_config.username.clone());
|
||||
retroachievements::init_client(
|
||||
if cfg!(ra_hardcore_enabled) && args.netplay_server_addr.is_none() {
|
||||
ra_config.hardcore
|
||||
} else {
|
||||
false
|
||||
},
|
||||
ra_config.challenge,
|
||||
ra_config.leaderboard,
|
||||
);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
if let Some(password) = args.ra_password {
|
||||
retroachievements::login_user(username, password, tx);
|
||||
} else if !ra_config.token.is_empty() {
|
||||
retroachievements::login_token_user(username, ra_config.token.clone(), tx);
|
||||
} else {
|
||||
tx.send(false).unwrap();
|
||||
}
|
||||
|
||||
rx.blocking_recv().unwrap();
|
||||
|
||||
ra_config
|
||||
} else {
|
||||
retroachievements::RAConfig::default()
|
||||
};
|
||||
|
||||
let netplay_config = if let Some(server_addr) = args.netplay_server_addr
|
||||
&& let Some(player_number) = args.netplay_player_number
|
||||
&& let Some(number_of_players) = args.netplay_number_of_players
|
||||
&& let Some(input_delay) = args.netplay_input_delay
|
||||
{
|
||||
Some(netplay::NetplayConfig {
|
||||
server_addr,
|
||||
player_number,
|
||||
number_of_players,
|
||||
input_delay,
|
||||
ice_config_path: dirs.cache_dir.join("ice_config.json"),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
device::run_game(
|
||||
&mut device,
|
||||
&rom_contents,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats,
|
||||
load_savestate_slot: args.load_state,
|
||||
},
|
||||
ra_config,
|
||||
netplay_config,
|
||||
);
|
||||
|
||||
// on Android, the client is shut down in the app_window function
|
||||
#[cfg(not(target_os = "android"))]
|
||||
retroachievements::shutdown_client();
|
||||
|
||||
if device.netplay.is_none() {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
&& !device.transferpaks[i].cart.ram.is_empty()
|
||||
{
|
||||
device::controller::gbcart::save(
|
||||
&mut device.transferpaks[i].cart,
|
||||
device.vi.elapsed_time as i64,
|
||||
&device.ui.config.input.gb_ram_path[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui::usb::close(shutdown_tx, usb_handle);
|
||||
} else if arg_count > 1 {
|
||||
let mut config = ui::config::Config::new();
|
||||
|
||||
if let Some(profile) = args.configure_input_profile {
|
||||
ui::input_profile::configure_input_profile(
|
||||
&mut config,
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input_profile::DEADZONE_DEFAULT),
|
||||
);
|
||||
|
||||
ui::sdl_close();
|
||||
return Ok(());
|
||||
} else {
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut config);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(port) = args.port
|
||||
&& !(1..=4).contains(&port)
|
||||
{
|
||||
return Err(Error::other("Port must be between 1 and 4"));
|
||||
}
|
||||
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
|
||||
if args.list_controllers {
|
||||
let controllers = ui::input::get_controller_names();
|
||||
for (i, controller) in controllers.iter().enumerate() {
|
||||
println!("Controller {i}: {controller}");
|
||||
}
|
||||
} else {
|
||||
if let Some(assign_controller) = args.assign_controller {
|
||||
let Some(port) = args.port else {
|
||||
ui::sdl_close();
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::assign_controller(&mut config, assign_controller - 1, port);
|
||||
}
|
||||
if let Some(profile) = args.bind_input_profile {
|
||||
let Some(port) = args.port else {
|
||||
ui::sdl_close();
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::bind_input_profile(&mut config, profile, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let app = ui::gui::AppWindow::new().unwrap();
|
||||
let no_intro_map =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
|
||||
set_app_id();
|
||||
ui::gui::app_window(&app, false, no_intro_map);
|
||||
}
|
||||
}
|
||||
|
||||
ui::sdl_close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_main(app: slint::android::AndroidApp) {
|
||||
let (close_tx, handle) = create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
slint::android::init_with_event_listener(app.clone(), move |event| match event {
|
||||
slint::android::android_activity::PollEvent::Main(main_event) => match main_event {
|
||||
slint::android::android_activity::MainEvent::TerminateWindow { .. } => {
|
||||
if let Ok(weak_app_window) = android::WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
weak_app_window
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
ui::gui::save_settings(&handle);
|
||||
handle.invoke_netplay_close();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
})
|
||||
.unwrap();
|
||||
let app_window = ui::gui::AppWindow::new().unwrap();
|
||||
*android::WEAK_SLINT_WINDOW.lock().unwrap() = Some(app_window.as_weak());
|
||||
|
||||
*android::ANDROID_APP.lock().unwrap() = Some(app);
|
||||
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
std::fs::create_dir_all(&dirs.config_dir).unwrap();
|
||||
std::fs::create_dir_all(&dirs.cache_dir).unwrap();
|
||||
std::fs::create_dir_all(dirs.data_dir.join("saves")).unwrap();
|
||||
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
|
||||
|
||||
let no_intro_map =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
|
||||
ui::gui::app_window(&app_window, true, no_intro_map);
|
||||
close_tx.send(()).unwrap();
|
||||
*android::WEAK_SLINT_WINDOW.lock().unwrap() = None;
|
||||
*android::ANDROID_APP.lock().unwrap() = None;
|
||||
}
|
||||
@@ -3,298 +3,14 @@
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
mod cheats;
|
||||
mod device;
|
||||
mod netplay;
|
||||
mod retroachievements;
|
||||
mod savestates;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
use std::io::Error;
|
||||
|
||||
/// N64 emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
|
||||
struct Args {
|
||||
game: Option<String>,
|
||||
#[arg(short, long)]
|
||||
fullscreen: bool,
|
||||
#[arg(long)]
|
||||
overclock: Option<bool>,
|
||||
#[arg(long)]
|
||||
disable_expansion_pak: Option<bool>,
|
||||
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
|
||||
cheats: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
|
||||
netplay_peer_addr: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
|
||||
netplay_player_number: Option<u8>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)"
|
||||
)]
|
||||
configure_input_profile: Option<String>,
|
||||
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
|
||||
use_dinput: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DEADZONE_PERCENTAGE",
|
||||
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
|
||||
)]
|
||||
deadzone: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Must also specify --port. Used to bind a previously created profile to a port"
|
||||
)]
|
||||
bind_input_profile: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Lists connected controllers which can be used in --assign-controller"
|
||||
)]
|
||||
list_controllers: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "CONTROLLER_NUMBER",
|
||||
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
|
||||
)]
|
||||
assign_controller: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PORT",
|
||||
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
|
||||
)]
|
||||
port: Option<usize>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Clear all input profile bindings and controller assignments"
|
||||
)]
|
||||
clear_input_bindings: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "SLOT",
|
||||
help = "Load savestate from slot 0-9 when starting the game"
|
||||
)]
|
||||
load_state: Option<u32>,
|
||||
#[arg(
|
||||
long = "ra-username",
|
||||
value_name = "USERNAME",
|
||||
help = "Username for RetroAchievements"
|
||||
)]
|
||||
ra_username: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-token",
|
||||
value_name = "TOKEN",
|
||||
help = "Token for RetroAchievements",
|
||||
hide = true
|
||||
)]
|
||||
ra_token: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-password",
|
||||
value_name = "PASSWORD",
|
||||
help = "Password for RetroAchievements"
|
||||
)]
|
||||
ra_password: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-hardcore",
|
||||
help = "Enable Hardcore mode for RetroAchievements"
|
||||
)]
|
||||
ra_hardcore: bool,
|
||||
#[arg(
|
||||
long = "ra-challenge",
|
||||
help = "Enable Challenge Indicators for RetroAchievements"
|
||||
)]
|
||||
ra_challenge: bool,
|
||||
#[arg(
|
||||
long = "ra-leaderboard",
|
||||
help = "Enable Leaderboard Trackers for RetroAchievements"
|
||||
)]
|
||||
ra_leaderboard: bool,
|
||||
}
|
||||
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
std::fs::create_dir_all(dirs.config_dir)?;
|
||||
std::fs::create_dir_all(dirs.cache_dir)?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
|
||||
let args = Args::parse();
|
||||
if let Some(game) = args.game {
|
||||
let file_path = std::path::Path::new(&game);
|
||||
let Some(rom_contents) = device::get_rom_contents(file_path) else {
|
||||
return Err(Error::other(format!(
|
||||
"Could not read ROM file: {}",
|
||||
file_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
if let Some(slot) = args.load_state
|
||||
&& slot > 9
|
||||
{
|
||||
return Err(Error::other("Savestate slot must be between 0 and 9"));
|
||||
}
|
||||
|
||||
let mut device = device::Device::new();
|
||||
|
||||
device.ui.config.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.recent_roms.insert(0, game);
|
||||
device.ui.config.recent_roms.truncate(5);
|
||||
|
||||
if args.fullscreen {
|
||||
device.ui.video.fullscreen = true;
|
||||
} else {
|
||||
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
|
||||
}
|
||||
let overclock = args
|
||||
.overclock
|
||||
.unwrap_or(device.ui.config.emulation.overclock);
|
||||
let disable_expansion_pak = args
|
||||
.disable_expansion_pak
|
||||
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
|
||||
let cheats = if let Some(cheats_file) = args.cheats
|
||||
&& let Ok(data) = std::fs::read(cheats_file)
|
||||
&& let Ok(cheats) = serde_json::from_slice(&data)
|
||||
{
|
||||
cheats
|
||||
} else {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
ui::config::Cheats::new()
|
||||
.cheats
|
||||
.get(&game_crc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let mut shutdown_tx = None;
|
||||
|
||||
if let Some(peer_addr) = args.netplay_peer_addr
|
||||
&& let Some(player_number) = args.netplay_player_number
|
||||
{
|
||||
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_rom_path[i].is_empty()
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
{
|
||||
device.transferpaks[i].cart.rom =
|
||||
std::fs::read(&device.ui.config.input.gb_rom_path[i]).unwrap();
|
||||
device.transferpaks[i].cart.ram =
|
||||
std::fs::read(&device.ui.config.input.gb_ram_path[i]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if device.ui.config.emulation.usb {
|
||||
(shutdown_tx, device.ui.usb) = ui::usb::init();
|
||||
}
|
||||
|
||||
if let Some(username) = args.ra_username {
|
||||
retroachievements::init_client(
|
||||
if cfg!(ra_hardcore_enabled) {
|
||||
args.ra_hardcore
|
||||
} else {
|
||||
false
|
||||
},
|
||||
args.ra_challenge,
|
||||
args.ra_leaderboard,
|
||||
);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
if let Some(password) = args.ra_password {
|
||||
retroachievements::login_user(username, password, tx);
|
||||
} else if let Some(token) = args.ra_token {
|
||||
retroachievements::login_token_user(username, token, tx);
|
||||
} else {
|
||||
tx.send(false).unwrap();
|
||||
}
|
||||
|
||||
rx.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
retroachievements::load_game(&rom_contents, rom_contents.len()).await;
|
||||
device::run_game(
|
||||
&mut device,
|
||||
&rom_contents,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats,
|
||||
load_savestate_slot: args.load_state,
|
||||
},
|
||||
);
|
||||
retroachievements::shutdown_client();
|
||||
|
||||
if device.netplay.is_some() {
|
||||
netplay::close(&mut device);
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
{
|
||||
tokio::fs::write(
|
||||
&device.ui.config.input.gb_ram_path[i],
|
||||
&device.transferpaks[i].cart.ram,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(shutdown_tx) = &shutdown_tx {
|
||||
ui::usb::close(shutdown_tx);
|
||||
}
|
||||
} else if std::env::args().count() > 1 {
|
||||
let mut ui = ui::Ui::new();
|
||||
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut ui);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(port) = args.port
|
||||
&& !(1..=4).contains(&port)
|
||||
{
|
||||
return Err(Error::other("Port must be between 1 and 4"));
|
||||
}
|
||||
if args.list_controllers {
|
||||
let controllers = ui::input::get_controller_names(&ui);
|
||||
for (i, controller) in controllers.iter().enumerate() {
|
||||
println!("Controller {i}: {controller}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(profile) = args.configure_input_profile {
|
||||
ui::input::configure_input_profile(
|
||||
&mut ui,
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(assign_controller) = args.assign_controller {
|
||||
let Some(port) = args.port else {
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::assign_controller(&mut ui, assign_controller, port);
|
||||
}
|
||||
if let Some(profile) = args.bind_input_profile {
|
||||
let Some(port) = args.port else {
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::bind_input_profile(&mut ui, profile, port);
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
retroachievements::init_client(false, false, false);
|
||||
ui::gui::app_window();
|
||||
retroachievements::shutdown_client();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn main() -> std::io::Result<()> {
|
||||
let (close_tx, handle) = gopher64::create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
let args = gopher64::Args::parse();
|
||||
let result = gopher64::run(args, std::env::args().count());
|
||||
close_tx.send(()).unwrap();
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,343 +1,586 @@
|
||||
use crate::{device, ui};
|
||||
use std::io::{Read, Write};
|
||||
//UDP packet formats
|
||||
const UDP_SEND_KEY_INFO: u8 = 0;
|
||||
const UDP_RECEIVE_KEY_INFO: u8 = 1;
|
||||
const UDP_REQUEST_KEY_INFO: u8 = 2;
|
||||
const UDP_RECEIVE_KEY_INFO_GRATUITOUS: u8 = 3;
|
||||
const UDP_SYNC_DATA: u8 = 4;
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use sha2::digest::Digest;
|
||||
|
||||
//TCP packet formats
|
||||
const TCP_SEND_SAVE: u8 = 1;
|
||||
//const TCP_RECEIVE_SAVE: u8 = 2;
|
||||
//const TCP_SEND_SETTINGS: u8 = 3;
|
||||
//const TCP_RECEIVE_SETTINGS: u8 = 4;
|
||||
const TCP_REGISTER_PLAYER: u8 = 5;
|
||||
const TCP_GET_REGISTRATION: u8 = 6;
|
||||
const TCP_DISCONNECT_NOTICE: u8 = 7;
|
||||
const TCP_RECEIVE_SAVE_WITH_SIZE: u8 = 8;
|
||||
const TCP_SEND_RTC: u8 = 64;
|
||||
const TCP_RECEIVE_RTC: u8 = 128;
|
||||
const TCP_SEND_RNG: u8 = 65;
|
||||
const TCP_RECEIVE_RNG: u8 = 129;
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RtcIceServerConfig {
|
||||
pub urls: Vec<String>,
|
||||
pub username: Option<String>,
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
const CS4: u32 = 32;
|
||||
pub struct GgrsConfig;
|
||||
impl ggrs::Config for GgrsConfig {
|
||||
type Input = ui::input::InputData;
|
||||
type InputPredictor = ggrs::PredictRepeatLast;
|
||||
type State = i32;
|
||||
type Address = matchbox_socket::PeerId;
|
||||
}
|
||||
|
||||
pub struct MatchboxChannel {
|
||||
channel: matchbox_socket::WebRtcChannel,
|
||||
}
|
||||
|
||||
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
|
||||
fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) {
|
||||
let encoded = postcard::to_stdvec(msg).expect("serialization failed");
|
||||
let _ = self.channel.try_send(encoded.into(), *addr);
|
||||
}
|
||||
|
||||
fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> {
|
||||
self.channel
|
||||
.receive()
|
||||
.iter()
|
||||
.filter_map(|(peer, packet)| {
|
||||
let msg = postcard::from_bytes::<ggrs::Message>(packet).ok()?;
|
||||
Some((*peer, msg))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetplayConfig {
|
||||
pub server_addr: String,
|
||||
pub player_number: usize,
|
||||
pub number_of_players: usize,
|
||||
pub input_delay: usize,
|
||||
pub ice_config_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub struct Netplay {
|
||||
udp_socket: std::net::UdpSocket,
|
||||
tcp_stream: std::net::TcpStream,
|
||||
pub player_number: u8,
|
||||
pub player_data: [PlayerData; 4],
|
||||
vi_counter: u32,
|
||||
status: u8,
|
||||
buffer_target: u8,
|
||||
pub fast_forward: bool,
|
||||
pub disconnected: bool,
|
||||
pub session: ggrs::P2PSession<GgrsConfig>,
|
||||
pub reliable_channel: matchbox_socket::WebRtcChannel,
|
||||
pub peers: Vec<matchbox_socket::PeerId>,
|
||||
pub player_number: usize,
|
||||
pub connected: [bool; 4],
|
||||
pub input_delay: usize,
|
||||
pub messages: std::collections::HashMap<String, Vec<u8>>,
|
||||
pub received_data: std::collections::VecDeque<Vec<u8>>,
|
||||
pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>,
|
||||
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
|
||||
pub incoming_message: Vec<u8>,
|
||||
pub ice_config_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub struct PlayerData {
|
||||
lag: u8,
|
||||
count: u32,
|
||||
pub reg_id: u32,
|
||||
input_events: std::collections::HashMap<u32, InputEvent>,
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct NetplayMessage {
|
||||
name: String,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
struct InputEvent {
|
||||
input: u32,
|
||||
plugin: u8,
|
||||
fn send_message(netplay: &mut Netplay, message: NetplayMessage) {
|
||||
let data = postcard::to_stdvec(&message).unwrap();
|
||||
let chunks = data.chunks(16384).collect::<Vec<&[u8]>>();
|
||||
for peer in netplay.peers.iter() {
|
||||
for chunk in chunks.iter() {
|
||||
if let Err(e) = netplay
|
||||
.reliable_channel
|
||||
.try_send(chunk.to_vec().into(), *peer)
|
||||
{
|
||||
eprintln!("Failed to send message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_reliable_messages(netplay: &mut Netplay) {
|
||||
netplay.received_data.extend(
|
||||
netplay
|
||||
.reliable_channel
|
||||
.receive()
|
||||
.iter()
|
||||
.map(|(_, data)| data.to_vec()),
|
||||
);
|
||||
|
||||
while !netplay.received_data.is_empty() {
|
||||
if let Some(data) = netplay.received_data.pop_front() {
|
||||
netplay.incoming_message.extend(data);
|
||||
|
||||
if let Ok(decoded_message) =
|
||||
postcard::from_bytes::<NetplayMessage>(&netplay.incoming_message)
|
||||
{
|
||||
netplay
|
||||
.messages
|
||||
.insert(decoded_message.name, decoded_message.data);
|
||||
netplay.incoming_message.clear();
|
||||
check_input_delay(netplay);
|
||||
check_disconnect(netplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_message(netplay: &mut Netplay, name: &str) -> Vec<u8> {
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
process_reliable_messages(netplay);
|
||||
if let Some(data) = netplay.messages.remove(name) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if now.elapsed() > timeout {
|
||||
panic!("Could not receive message for {name}");
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_player_number(
|
||||
channel: &mut matchbox_socket::WebRtcChannel,
|
||||
peers: &[matchbox_socket::PeerId],
|
||||
player_number: usize,
|
||||
) {
|
||||
let message = NetplayMessage {
|
||||
name: "player_number".to_string(),
|
||||
data: (player_number as u64).to_be_bytes().to_vec(),
|
||||
};
|
||||
let data = postcard::to_stdvec(&message).unwrap();
|
||||
for peer in peers {
|
||||
if let Err(e) = channel.try_send(data.clone().into(), *peer) {
|
||||
eprintln!("Failed to send message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_player_numbers(
|
||||
channel: &mut matchbox_socket::WebRtcChannel,
|
||||
player_numbers: &mut std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
|
||||
) {
|
||||
for (peer, data) in channel.receive() {
|
||||
let message = postcard::from_bytes::<NetplayMessage>(&data).unwrap();
|
||||
if message.name == "player_number" {
|
||||
player_numbers.insert(
|
||||
u64::from_be_bytes(message.data.try_into().unwrap()) as usize,
|
||||
Some(peer),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
|
||||
let size: u32 = 8;
|
||||
request.extend_from_slice(&size.to_be_bytes());
|
||||
request.extend_from_slice(&rtc.to_be_bytes());
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let message = NetplayMessage {
|
||||
name: "rtc".to_string(),
|
||||
data: rtc.to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_rtc(netplay: &mut Netplay) -> i64 {
|
||||
let request: Vec<u8> = [TCP_RECEIVE_RTC].to_vec();
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let mut rtc: [u8; 8] = [0; 8];
|
||||
netplay.tcp_stream.read_exact(&mut rtc).unwrap();
|
||||
i64::from_be_bytes(rtc)
|
||||
let message = receive_message(netplay, "rtc");
|
||||
|
||||
i64::from_be_bytes(message.try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn send_rng(netplay: &mut Netplay, seed: u64) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_RNG].to_vec();
|
||||
let size: u32 = 8;
|
||||
request.extend_from_slice(&size.to_be_bytes());
|
||||
request.extend_from_slice(&seed.to_be_bytes());
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let message = NetplayMessage {
|
||||
name: "rng".to_string(),
|
||||
data: seed.to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_rng(netplay: &mut Netplay) -> u64 {
|
||||
let request: Vec<u8> = [TCP_RECEIVE_RNG].to_vec();
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let mut seed: [u8; 8] = [0; 8];
|
||||
netplay.tcp_stream.read_exact(&mut seed).unwrap();
|
||||
u64::from_be_bytes(seed)
|
||||
let message = receive_message(netplay, "rng");
|
||||
u64::from_be_bytes(message.try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size: usize) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_SAVE].to_vec();
|
||||
request.extend_from_slice(save_type.as_bytes());
|
||||
request.push(0); // null terminate string
|
||||
request.extend_from_slice(&(size as u32).to_be_bytes());
|
||||
|
||||
if size > 0 {
|
||||
request.extend_from_slice(save_data);
|
||||
}
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8]) {
|
||||
let message = NetplayMessage {
|
||||
name: save_type.to_string(),
|
||||
data: save_data.to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<u8>) {
|
||||
let mut request: Vec<u8> = [TCP_RECEIVE_SAVE_WITH_SIZE].to_vec();
|
||||
request.extend_from_slice(save_type.as_bytes());
|
||||
request.push(0); // null terminate string
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
|
||||
let mut size: [u8; 4] = [0; 4];
|
||||
netplay.tcp_stream.read_exact(&mut size).unwrap();
|
||||
let mut response: Vec<u8> = vec![0; u32::from_be_bytes(size) as usize];
|
||||
netplay.tcp_stream.read_exact(&mut response).unwrap();
|
||||
*save_data = response;
|
||||
let message = receive_message(netplay, save_type);
|
||||
*save_data = message;
|
||||
}
|
||||
|
||||
pub fn send_sync_check(netplay: &mut Netplay, regs: &[u64]) {
|
||||
if netplay.vi_counter.is_multiple_of(600) {
|
||||
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
|
||||
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
|
||||
|
||||
for item in regs.iter() {
|
||||
request.extend_from_slice(&(*item as u32).to_be_bytes());
|
||||
}
|
||||
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
netplay.vi_counter = netplay.vi_counter.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn send_input(netplay: &Netplay, input: ui::input::InputData) {
|
||||
let mut request: Vec<u8> = [UDP_SEND_KEY_INFO].to_vec();
|
||||
request.push(netplay.player_number);
|
||||
request.extend_from_slice(
|
||||
&(netplay.player_data[netplay.player_number as usize].count).to_be_bytes(),
|
||||
);
|
||||
request.extend_from_slice(&(input.data).to_be_bytes());
|
||||
request.push(input.pak_change_pressed as u8);
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::InputData {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let mut input = None;
|
||||
|
||||
let timeout = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
let mut request_timer = std::time::Instant::now() - std::time::Duration::from_millis(5);
|
||||
while input.is_none() {
|
||||
process_incoming(netplay); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
if std::time::Instant::now() > request_timer {
|
||||
// sends a request packet every 5ms
|
||||
request_input(netplay, channel);
|
||||
request_timer = std::time::Instant::now() + std::time::Duration::from_millis(5);
|
||||
}
|
||||
input = netplay.player_data[channel]
|
||||
.input_events
|
||||
.remove(&netplay.player_data[channel].count);
|
||||
|
||||
if std::time::Instant::now() > timeout {
|
||||
ui::video::onscreen_message("Lost connection to netplay server", false);
|
||||
input = Some(InputEvent {
|
||||
input: 0,
|
||||
plugin: 0,
|
||||
});
|
||||
device.cpu.running = false;
|
||||
}
|
||||
pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
|
||||
if input_delay < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
netplay.fast_forward = netplay.player_data[channel].lag > 0
|
||||
&& netplay.player_data[channel].input_events.len() as u8 > netplay.buffer_target;
|
||||
|
||||
netplay.player_data[channel].count = netplay.player_data[channel].count.wrapping_add(1);
|
||||
ui::input::InputData {
|
||||
data: input.as_ref().unwrap().input,
|
||||
pak_change_pressed: input.as_ref().unwrap().plugin != 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn request_input(netplay: &Netplay, channel: usize) {
|
||||
let mut request: Vec<u8> = [UDP_REQUEST_KEY_INFO].to_vec();
|
||||
request.push(channel as u8); //The player we need input for
|
||||
request.extend_from_slice(
|
||||
&(netplay.player_data[netplay.player_number as usize].reg_id).to_be_bytes(),
|
||||
);
|
||||
request.extend_from_slice(&(netplay.player_data[channel].count).to_be_bytes());
|
||||
request.push(0); //spectator mode
|
||||
request.push(netplay.player_data[channel].input_events.len() as u8);
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
fn process_incoming(netplay: &mut Netplay) {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
|
||||
match buf[0] {
|
||||
UDP_RECEIVE_KEY_INFO | UDP_RECEIVE_KEY_INFO_GRATUITOUS => {
|
||||
let player = buf[1] as usize;
|
||||
//current_status is a status update from the server
|
||||
//it will let us know if another player has disconnected, or the games have desynced
|
||||
let current_status = buf[2];
|
||||
if buf[0] == UDP_RECEIVE_KEY_INFO {
|
||||
netplay.player_data[player].lag = buf[3];
|
||||
}
|
||||
if current_status != netplay.status {
|
||||
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
|
||||
ui::video::onscreen_message("Netplay desync detected", true);
|
||||
}
|
||||
for dis in 1..5 {
|
||||
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
|
||||
{
|
||||
ui::video::onscreen_message(
|
||||
&format!("Player {dis} disconnected"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
netplay.status = current_status;
|
||||
}
|
||||
|
||||
let mut buffer_offset = 5;
|
||||
for _i in 0..buf[4] {
|
||||
let count = u32::from_be_bytes(
|
||||
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
|
||||
);
|
||||
buffer_offset += 4;
|
||||
|
||||
if (count.wrapping_sub(netplay.player_data[player].count)) > (u32::MAX / 2) {
|
||||
//event doesn't need to be recorded
|
||||
buffer_offset += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
let input = u32::from_be_bytes(
|
||||
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
|
||||
);
|
||||
buffer_offset += 4;
|
||||
let plugin = buf[buffer_offset];
|
||||
buffer_offset += 1;
|
||||
let input_event = InputEvent { input, plugin };
|
||||
netplay.player_data[player]
|
||||
.input_events
|
||||
.insert(count, input_event);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown UDP packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
|
||||
let udp_socket;
|
||||
if peer_addr.is_ipv4() {
|
||||
udp_socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
|
||||
.expect("couldn't bind to address");
|
||||
socket2::SockRef::from(&udp_socket)
|
||||
.set_tos_v4(CS4 << 2)
|
||||
.unwrap();
|
||||
} else {
|
||||
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
|
||||
.expect("couldn't bind to address");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
socket2::SockRef::from(&udp_socket)
|
||||
.set_tclass_v6(CS4 << 2)
|
||||
.unwrap();
|
||||
let message = NetplayMessage {
|
||||
name: "input_delay".to_string(),
|
||||
data: (input_delay as u64).to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
change_input_delay(netplay, input_delay);
|
||||
}
|
||||
|
||||
udp_socket.connect(peer_addr).unwrap();
|
||||
udp_socket.set_nonblocking(true).unwrap();
|
||||
|
||||
let mut stream = std::net::TcpStream::connect(peer_addr).unwrap();
|
||||
|
||||
let regid = (player_number + 1) as u32;
|
||||
let mut request: [u8; 8] = [
|
||||
TCP_REGISTER_PLAYER,
|
||||
player_number,
|
||||
0, //plugin/pak
|
||||
0, //rawdata
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
];
|
||||
|
||||
request[4..8].copy_from_slice(®id.to_be_bytes());
|
||||
stream.write_all(&request).unwrap();
|
||||
|
||||
let mut response: [u8; 2] = [0, 0];
|
||||
stream.read_exact(&mut response).unwrap();
|
||||
if response[0] != 1 {
|
||||
panic!("Failed to register player");
|
||||
}
|
||||
let buffer_target = response[1];
|
||||
|
||||
let request: [u8; 1] = [TCP_GET_REGISTRATION];
|
||||
stream.write_all(&request).unwrap();
|
||||
let mut response: [u8; 24] = [0; 24];
|
||||
stream.read_exact(&mut response).unwrap();
|
||||
|
||||
let mut reg_id: [u32; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
// reg_id of 0 means no player connected
|
||||
reg_id[i] = u32::from_be_bytes(response[(i * 6)..(i * 6) + 4].try_into().unwrap());
|
||||
}
|
||||
Netplay {
|
||||
udp_socket,
|
||||
tcp_stream: stream,
|
||||
player_number,
|
||||
vi_counter: 0,
|
||||
status: 0,
|
||||
buffer_target,
|
||||
fast_forward: false,
|
||||
player_data: [
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[0],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[1],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[2],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[3],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
],
|
||||
fn change_input_delay(netplay: &mut Netplay, input_delay: usize) {
|
||||
netplay.input_delay = input_delay;
|
||||
for handle in netplay.session.local_player_handles() {
|
||||
if let Err(e) = netplay.session.set_input_delay(handle, input_delay) {
|
||||
eprintln!("Error setting input delay: {}", e);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!("Input delay: {}", input_delay),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let regid = (netplay.player_number + 1) as u32;
|
||||
let mut request: [u8; 5] = [TCP_DISCONNECT_NOTICE, 0, 0, 0, 0];
|
||||
request[1..5].copy_from_slice(®id.to_be_bytes());
|
||||
fn check_input_delay(netplay: &mut Netplay) {
|
||||
if let Some(data) = netplay.messages.remove("input_delay") {
|
||||
let input_delay = u64::from_be_bytes(data.try_into().unwrap()) as usize;
|
||||
if input_delay != netplay.input_delay {
|
||||
change_input_delay(netplay, input_delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
netplay.tcp_stream.flush().unwrap();
|
||||
fn check_disconnect(netplay: &mut Netplay) {
|
||||
if !netplay.disconnected && netplay.messages.remove("disconnect").is_some() {
|
||||
netplay.disconnected = true;
|
||||
ui::video::onscreen_message(
|
||||
"Player disconnected, session has ended",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_frames(netplay: &Netplay) -> usize {
|
||||
netplay
|
||||
.tcp_stream
|
||||
.shutdown(std::net::Shutdown::Both)
|
||||
.unwrap();
|
||||
.requests
|
||||
.iter()
|
||||
.filter(|r| matches!(r, ggrs::GgrsRequest::AdvanceFrame { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn in_rollback(netplay: Option<&Netplay>) -> bool {
|
||||
if let Some(netplay) = netplay {
|
||||
pending_frames(netplay) != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_requests(
|
||||
device: &mut device::Device,
|
||||
) -> Vec<(ui::input::InputData, ggrs::InputStatus)> {
|
||||
loop {
|
||||
if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() {
|
||||
match request {
|
||||
ggrs::GgrsRequest::SaveGameState { cell, frame } => {
|
||||
//savestates::create_savestate(device, true, Some(frame));
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
for reg in device.cpu.cop0.regs.as_ref() {
|
||||
hasher.update(reg.to_be_bytes());
|
||||
}
|
||||
let hash = u128::from_be_bytes(hasher.finalize()[..16].try_into().unwrap());
|
||||
cell.save(frame, Some(frame), Some(hash));
|
||||
}
|
||||
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
|
||||
eprintln!("attempting to load game state");
|
||||
if let Some(_frame) = cell.load() {
|
||||
// savestates::load_savestate(device, true, Some(frame));
|
||||
}
|
||||
}
|
||||
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
|
||||
return inputs;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process_netplay(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_clients(netplay: &mut Netplay) {
|
||||
netplay.session.poll_remote_clients();
|
||||
for event in netplay.session.events() {
|
||||
match event {
|
||||
ggrs::GgrsEvent::Synchronizing { .. } => {}
|
||||
ggrs::GgrsEvent::Synchronized { .. } => {}
|
||||
ggrs::GgrsEvent::Disconnected { .. } => {
|
||||
if !netplay.disconnected {
|
||||
netplay.disconnected = true;
|
||||
ui::video::onscreen_message(
|
||||
"Lost connection to peer(s)",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
ggrs::GgrsEvent::NetworkInterrupted { .. } => {
|
||||
println!("network interrupted");
|
||||
}
|
||||
ggrs::GgrsEvent::NetworkResumed { .. } => {
|
||||
println!("network resumed");
|
||||
}
|
||||
ggrs::GgrsEvent::WaitRecommendation { skip_frames } => {
|
||||
println!("wait recommendation: skip_frames={}", skip_frames);
|
||||
}
|
||||
ggrs::GgrsEvent::DesyncDetected { .. } => {
|
||||
eprintln!("desync detected");
|
||||
ui::video::onscreen_message(
|
||||
"Desync detected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_netplay(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
|
||||
poll_clients(netplay);
|
||||
process_reliable_messages(netplay);
|
||||
advance_frame(device);
|
||||
}
|
||||
|
||||
fn advance_frame(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let local_input = if netplay.session.current_frame() > netplay.session.max_prediction() as i32 {
|
||||
ui::input::get(&mut device.ui, 0)
|
||||
} else {
|
||||
//workaround for disabled rollback
|
||||
ui::input::InputData::default()
|
||||
};
|
||||
let local_handle = *netplay.session.local_player_handles().first().unwrap();
|
||||
netplay
|
||||
.session
|
||||
.add_local_input(local_handle, local_input)
|
||||
.unwrap();
|
||||
|
||||
// avoid rollback
|
||||
while !netplay.disconnected
|
||||
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
|
||||
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
|
||||
{
|
||||
poll_clients(netplay);
|
||||
}
|
||||
|
||||
if netplay.disconnected {
|
||||
netplay.requests.push_back(ggrs::GgrsRequest::AdvanceFrame {
|
||||
inputs: vec![
|
||||
(
|
||||
ui::input::InputData::default(),
|
||||
ggrs::InputStatus::Disconnected
|
||||
);
|
||||
4
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
match netplay.session.advance_frame() {
|
||||
Ok(requests) => {
|
||||
netplay.requests.extend(requests);
|
||||
}
|
||||
Err(ggrs::GgrsError::PredictionThreshold) => {
|
||||
println!("prediction threshold reached");
|
||||
}
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_peers(
|
||||
peers: &[matchbox_socket::PeerId],
|
||||
player_numbers: &std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
|
||||
) -> bool {
|
||||
for peer in player_numbers.values() {
|
||||
if let Some(peer) = peer
|
||||
&& !peers.contains(peer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn create_socket(builder: matchbox_socket::WebRtcSocketBuilder) -> matchbox_socket::WebRtcSocket {
|
||||
let (socket, loop_fut) = builder.build();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = loop_fut.await {
|
||||
eprintln!("WebRTC loop failed: {}", e);
|
||||
}
|
||||
});
|
||||
socket
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
device: &mut device::Device,
|
||||
netplay_config: &NetplayConfig,
|
||||
pal: bool,
|
||||
) -> Option<Netplay> {
|
||||
let mut builder = matchbox_socket::WebRtcSocketBuilder::new(&netplay_config.server_addr)
|
||||
.add_unreliable_channel()
|
||||
.add_reliable_channel();
|
||||
|
||||
if let Ok(ice_config) = std::fs::read(&netplay_config.ice_config_path)
|
||||
&& let Ok(ice_config) = serde_json::from_slice::<RtcIceServerConfig>(&ice_config)
|
||||
{
|
||||
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
|
||||
urls: ice_config.urls,
|
||||
username: ice_config.username,
|
||||
credential: ice_config.credential,
|
||||
});
|
||||
} else {
|
||||
eprintln!("Using default ICE config");
|
||||
}
|
||||
|
||||
let mut socket = create_socket(builder.clone());
|
||||
|
||||
let mut now = std::time::Instant::now();
|
||||
let mut message_timer = now;
|
||||
let socket_timeout = std::time::Duration::from_secs_f64(rand::random_range(8.0..10.0));
|
||||
let mut player_numbers =
|
||||
std::collections::BTreeMap::from([(netplay_config.player_number, None)]);
|
||||
|
||||
ui::video::onscreen_message(
|
||||
"Connecting to netplay peers...\nPlease wait...",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
|
||||
device.cpu.running = true;
|
||||
while device.cpu.running {
|
||||
if socket
|
||||
.update_peers()
|
||||
.iter()
|
||||
.any(|(_, peer_state)| *peer_state == matchbox_socket::PeerState::Disconnected)
|
||||
{
|
||||
// if someone has disconnected, reset the timeout
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
|
||||
let connected_peers = socket
|
||||
.connected_peers()
|
||||
.collect::<Vec<matchbox_socket::PeerId>>();
|
||||
|
||||
if connected_peers.len() == netplay_config.number_of_players - 1 {
|
||||
send_player_number(
|
||||
socket.channel_mut(1),
|
||||
&connected_peers,
|
||||
netplay_config.player_number,
|
||||
);
|
||||
get_player_numbers(socket.channel_mut(1), &mut player_numbers);
|
||||
if player_numbers.len() == netplay_config.number_of_players
|
||||
&& verify_peers(&connected_peers, &player_numbers)
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else if now.elapsed() > socket_timeout {
|
||||
socket.close();
|
||||
player_numbers.retain(|_, peer| peer.is_none());
|
||||
socket = create_socket(builder.clone());
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
|
||||
if message_timer.elapsed() > std::time::Duration::from_secs(4) {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Still connecting to {} netplay peer(s)...\nPlease wait...",
|
||||
netplay_config.number_of_players - player_numbers.len()
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
message_timer = std::time::Instant::now();
|
||||
}
|
||||
|
||||
ui::video::render_frame();
|
||||
ui::video::update_screen();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
ui::video::check_callback(device);
|
||||
}
|
||||
if !device.cpu.running {
|
||||
// user closed the window
|
||||
return None;
|
||||
}
|
||||
device.cpu.running = false;
|
||||
|
||||
let mut session_builder = ggrs::SessionBuilder::<GgrsConfig>::new()
|
||||
.with_num_players(netplay_config.number_of_players)
|
||||
.unwrap()
|
||||
.with_input_delay(netplay_config.input_delay)
|
||||
.with_fps(if pal { 50 } else { 60 })
|
||||
.unwrap()
|
||||
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 })
|
||||
.with_max_prediction_window(16)
|
||||
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
|
||||
10
|
||||
} else {
|
||||
5
|
||||
}));
|
||||
|
||||
let mut peers = vec![];
|
||||
for (i, peer) in player_numbers.iter() {
|
||||
if let Some(peer) = peer {
|
||||
session_builder = session_builder
|
||||
.add_player(ggrs::PlayerType::Remote(*peer), *i)
|
||||
.unwrap();
|
||||
peers.push(*peer);
|
||||
} else {
|
||||
session_builder = session_builder
|
||||
.add_player(ggrs::PlayerType::Local, *i)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let matchbox_channel = MatchboxChannel {
|
||||
channel: socket.take_channel(0).unwrap(),
|
||||
};
|
||||
let reliable_channel = socket.take_channel(1).unwrap();
|
||||
|
||||
if matchbox_channel.channel.config().max_retransmits != Some(0)
|
||||
|| matchbox_channel.channel.config().ordered
|
||||
{
|
||||
eprintln!("Sending GGRS traffic over reliable channel");
|
||||
}
|
||||
|
||||
let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
while session.current_state() != ggrs::SessionState::Running {
|
||||
session.poll_remote_clients();
|
||||
if now.elapsed() > timeout {
|
||||
eprintln!("Could not start netplay session");
|
||||
return None;
|
||||
}
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
Some(Netplay {
|
||||
disconnected: false,
|
||||
incoming_message: vec![],
|
||||
input_delay: netplay_config.input_delay,
|
||||
session,
|
||||
reliable_channel,
|
||||
peers,
|
||||
player_number: netplay_config.player_number,
|
||||
connected: [
|
||||
netplay_config.number_of_players > 0,
|
||||
netplay_config.number_of_players > 1,
|
||||
netplay_config.number_of_players > 2,
|
||||
netplay_config.number_of_players > 3,
|
||||
],
|
||||
inputs: Vec::new(),
|
||||
requests: std::collections::VecDeque::new(),
|
||||
received_data: std::collections::VecDeque::new(),
|
||||
messages: std::collections::HashMap::new(),
|
||||
ice_config_path: netplay_config.ice_config_path.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn close(netplay: &mut Netplay) {
|
||||
if !netplay.disconnected {
|
||||
let message = NetplayMessage {
|
||||
name: "disconnect".to_string(),
|
||||
data: vec![],
|
||||
};
|
||||
send_message(netplay, message);
|
||||
std::thread::sleep(std::time::Duration::from_millis(200)); // give the message time to be sent
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&netplay.ice_config_path);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,9 @@ include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
|
||||
|
||||
use crate::ui;
|
||||
|
||||
static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(format!(
|
||||
"{}/{}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("GIT_DESCRIBE")
|
||||
))
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
use discord_rich_presence::DiscordIpc;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct RAConfig {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
@@ -21,6 +12,7 @@ pub struct RAConfig {
|
||||
pub hardcore: bool,
|
||||
pub challenge: bool,
|
||||
pub leaderboard: bool,
|
||||
pub rich_presence: bool,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -53,6 +45,7 @@ pub extern "C" fn store_retroachievements_credentials(
|
||||
hardcore: result.hardcore,
|
||||
challenge: result.challenge,
|
||||
leaderboard: result.leaderboard,
|
||||
rich_presence: result.rich_presence,
|
||||
}
|
||||
} else {
|
||||
RAConfig {
|
||||
@@ -62,6 +55,7 @@ pub extern "C" fn store_retroachievements_credentials(
|
||||
hardcore: false,
|
||||
challenge: false,
|
||||
leaderboard: false,
|
||||
rich_presence: false,
|
||||
}
|
||||
};
|
||||
let f = std::fs::File::create(&file_path).unwrap();
|
||||
@@ -85,13 +79,13 @@ pub extern "C" fn rust_server_call(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_post_data).to_str().unwrap() }.to_string();
|
||||
let content_type =
|
||||
unsafe { std::ffi::CStr::from_ptr(c_content_type).to_str().unwrap() }.to_string();
|
||||
WEB_CLIENT
|
||||
ui::WEB_CLIENT
|
||||
.post(url)
|
||||
.body(post_data)
|
||||
.header(reqwest::header::CONTENT_TYPE, content_type)
|
||||
.send()
|
||||
} else {
|
||||
WEB_CLIENT.get(url).send()
|
||||
ui::WEB_CLIENT.get(url).send()
|
||||
};
|
||||
let callback = c_callback.addr();
|
||||
let callback_data = c_callback_data.addr();
|
||||
@@ -143,25 +137,83 @@ pub extern "C" fn rust_server_call(
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn load_game(rom: &[u8], rom_size: usize) {
|
||||
pub fn load_game(
|
||||
rom: &[u8],
|
||||
rom_size: usize,
|
||||
discord_rich_presence: bool,
|
||||
) -> (
|
||||
Option<tokio::sync::watch::Sender<()>>,
|
||||
Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
unsafe {
|
||||
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
|
||||
ra_load_game(rom.as_ptr(), rom_size, tx_ptr);
|
||||
};
|
||||
rx.await.unwrap();
|
||||
|
||||
rx.blocking_recv().unwrap();
|
||||
|
||||
let mut c_title = std::ptr::null();
|
||||
let mut c_image_url = std::ptr::null();
|
||||
unsafe { ra_welcome(&mut c_title, &mut c_image_url) };
|
||||
|
||||
if !discord_rich_presence || c_title.is_null() || c_image_url.is_null() {
|
||||
(None, None)
|
||||
} else {
|
||||
let (discord_watch_tx, discord_watch_rx) = tokio::sync::watch::channel(());
|
||||
(
|
||||
Some(discord_watch_tx),
|
||||
Some(init_rich_presence(
|
||||
discord_watch_rx,
|
||||
unsafe { std::ffi::CStr::from_ptr(c_title) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
unsafe { std::ffi::CStr::from_ptr(c_image_url) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unload_game() {
|
||||
pub fn unload_game(
|
||||
discord_watch_tx: Option<tokio::sync::watch::Sender<()>>,
|
||||
discord_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
if let Some(mut discord_handle) = discord_handle
|
||||
&& let Some(discord_watch_tx) = discord_watch_tx
|
||||
{
|
||||
let _ = discord_watch_tx.send(());
|
||||
tokio::task::spawn(async move {
|
||||
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut discord_handle)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
discord_handle.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
unsafe { ra_unload_game() };
|
||||
}
|
||||
|
||||
pub fn welcome() {
|
||||
unsafe { ra_welcome() };
|
||||
pub fn get_rich_presence() -> Option<String> {
|
||||
let c_rich_presence = unsafe { ra_get_rich_presence() };
|
||||
if c_rich_presence.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_rich_presence) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
|
||||
unsafe { ra_set_dmem(dmem, dmem_size) };
|
||||
pub fn set_rdram(rdram: *const u8, rdram_size: usize) {
|
||||
unsafe { ra_set_rdram(rdram, rdram_size) };
|
||||
}
|
||||
|
||||
pub fn do_frame() {
|
||||
@@ -222,21 +274,6 @@ pub fn get_username() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn get_token() -> Option<String> {
|
||||
let c_token = unsafe { ra_get_token() };
|
||||
if c_token.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_token) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_size() -> usize {
|
||||
unsafe { ra_state_size() }
|
||||
}
|
||||
@@ -248,3 +285,52 @@ pub fn save_state(state: *mut u8, state_size: usize) {
|
||||
pub fn load_state(state: *const u8, state_size: usize) {
|
||||
unsafe { ra_load_state(state, state_size) };
|
||||
}
|
||||
|
||||
pub fn init_rich_presence(
|
||||
mut discord_watch_rx: tokio::sync::watch::Receiver<()>,
|
||||
game_title: String,
|
||||
game_image_url: String,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut client = discord_rich_presence::DiscordIpcClient::new("1395482226463870986");
|
||||
let timestamps = discord_rich_presence::activity::Timestamps::new().start(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64,
|
||||
);
|
||||
|
||||
if let Err(e) = client.connect() {
|
||||
eprintln!("Failed to connect to Discord: {e}");
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = discord_watch_rx.changed() => {
|
||||
if let Err(e) = client.clear_activity() {
|
||||
eprintln!("Failed to clear Discord activity: {e}");
|
||||
}
|
||||
if let Err(e) = client.close() {
|
||||
eprintln!("Failed to close Discord: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
if let Some(rich_presence) = get_rich_presence()
|
||||
&& let Err(e) = client.set_activity(
|
||||
discord_rich_presence::activity::Activity::new()
|
||||
.details(game_title.clone())
|
||||
.state(rich_presence)
|
||||
.assets(
|
||||
discord_rich_presence::activity::Assets::new()
|
||||
.small_image(game_image_url.clone()),
|
||||
).timestamps(timestamps.clone()),
|
||||
)
|
||||
{
|
||||
eprintln!("Failed to set Discord activity: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
|
||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
use std::ops::Deref;
|
||||
|
||||
struct M128iArrayVisitor<const N: usize>;
|
||||
|
||||
@@ -69,172 +70,326 @@ where
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn create_savestate(device: &device::Device) {
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Savestate {
|
||||
pub save_state: bool,
|
||||
pub load_state: bool,
|
||||
pub load_rewind: bool,
|
||||
pub last_rewind_saved: f64,
|
||||
#[serde(skip)]
|
||||
pub rewind_pool:
|
||||
std::sync::Arc<tokio::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
|
||||
}
|
||||
|
||||
pub struct SavestateData {
|
||||
device: Box<device::Device>,
|
||||
saves: ui::storage::Saves,
|
||||
rdp_state: Vec<u8>,
|
||||
ra_state: Vec<u8>,
|
||||
}
|
||||
|
||||
static DEVICE_CLONE: std::sync::LazyLock<tokio::sync::Mutex<Box<device::Device>>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(device::Device::new(false)));
|
||||
|
||||
static SAVES_CLONE: std::sync::LazyLock<tokio::sync::Mutex<ui::storage::Saves>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(ui::storage::Saves::default()));
|
||||
|
||||
pub fn process_savestates(device: &mut device::Device) {
|
||||
let save_rewind = if device.netplay.is_none()
|
||||
&& device.ui.config.emulation.rewind
|
||||
&& device.vi.elapsed_time - device.savestate.last_rewind_saved > 1.0
|
||||
{
|
||||
device.savestate.last_rewind_saved = device.vi.elapsed_time;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if device.savestate.save_state || save_rewind {
|
||||
create_savestate(device, !device.savestate.save_state, None);
|
||||
device.savestate.save_state = false;
|
||||
} else if device.savestate.load_state || device.savestate.load_rewind {
|
||||
load_savestate(device, device.savestate.load_rewind, None);
|
||||
device.savestate.load_state = false;
|
||||
device.savestate.load_rewind = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
|
||||
if !rewind {
|
||||
ui::video::idle();
|
||||
}
|
||||
|
||||
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
|
||||
ui::video::save_state(rdp_state.as_mut_ptr());
|
||||
|
||||
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
|
||||
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
|
||||
|
||||
let data: &[(&[u8], &str)] = &[
|
||||
(&postcard::to_stdvec(device).unwrap(), "device"),
|
||||
(
|
||||
&postcard::to_stdvec(&device.ui.storage.saves).unwrap(),
|
||||
"saves",
|
||||
),
|
||||
(&rdp_state, "rdp_state"),
|
||||
(&ra_state, "ra_state"),
|
||||
];
|
||||
DEVICE_CLONE.blocking_lock().clone_state(device);
|
||||
SAVES_CLONE
|
||||
.blocking_lock()
|
||||
.clone_from(&device.ui.storage.saves);
|
||||
|
||||
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
||||
let compressed_file = ui::storage::compress_file(data);
|
||||
let save_state_slot = device.ui.storage.save_state_slot;
|
||||
let rewind_pool = device.savestate.rewind_pool.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::write(save_path, compressed_file).await.unwrap();
|
||||
let mut error = false;
|
||||
|
||||
if rewind {
|
||||
let mut pool = rewind_pool.lock().await;
|
||||
let mut state = device::Device::new(false);
|
||||
state.clone_state(DEVICE_CLONE.lock().await.deref());
|
||||
let key = if let Some(key) = rewind_frame {
|
||||
key
|
||||
} else if let Some(key) = pool.keys().last() {
|
||||
key + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
pool.insert(
|
||||
key,
|
||||
SavestateData {
|
||||
device: state,
|
||||
saves: SAVES_CLONE.lock().await.clone(),
|
||||
rdp_state,
|
||||
ra_state,
|
||||
},
|
||||
);
|
||||
if pool.len() > 30 {
|
||||
pool.pop_first();
|
||||
}
|
||||
} else {
|
||||
let compressed_file = if let Ok(device_data) =
|
||||
postcard::to_stdvec(DEVICE_CLONE.lock().await.deref())
|
||||
&& let Ok(saves_data) = postcard::to_stdvec(SAVES_CLONE.lock().await.deref())
|
||||
&& let Ok(compressed_file) = ui::storage::compress_file(&[
|
||||
(&device_data, "device"),
|
||||
(&saves_data, "saves"),
|
||||
(&rdp_state, "rdp_state"),
|
||||
(&ra_state, "ra_state"),
|
||||
]) {
|
||||
Some(compressed_file)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(compressed_file) = compressed_file {
|
||||
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
|
||||
eprintln!("Error writing savestate: {}", e);
|
||||
error = true;
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
eprintln!("Error compressing savestate");
|
||||
}
|
||||
}
|
||||
|
||||
if error {
|
||||
ui::video::onscreen_message(
|
||||
&format!("Failed to create savestate in slot {}", save_state_slot),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if !rewind {
|
||||
ui::video::onscreen_message(
|
||||
&format!("Savestate created in slot {}", save_state_slot),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate created in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn load_savestate(device: &mut device::Device) {
|
||||
pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message("Cannot load savestate in RA hardcore mode", false);
|
||||
ui::video::onscreen_message(
|
||||
"Cannot load savestate in RA hardcore mode",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
|
||||
if let Ok(savestate) = &savestate {
|
||||
let device_bytes = ui::storage::decompress_file(savestate, "device");
|
||||
let save_bytes = ui::storage::decompress_file(savestate, "saves");
|
||||
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
|
||||
let ra_state = ui::storage::decompress_file(savestate, "ra_state");
|
||||
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
|
||||
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
|
||||
|
||||
device.cpu = state.cpu;
|
||||
device.pif = state.pif;
|
||||
|
||||
let rom = device.cart.rom.clone();
|
||||
device.cart = state.cart;
|
||||
device.cart.rom = rom;
|
||||
|
||||
device.memory = state.memory;
|
||||
device.rsp = state.rsp;
|
||||
device.rdp = state.rdp;
|
||||
device.rdram = state.rdram;
|
||||
device.mi = state.mi;
|
||||
device.pi = state.pi;
|
||||
device.vi = state.vi;
|
||||
device.ai = state.ai;
|
||||
device.si = state.si;
|
||||
device.ri = state.ri;
|
||||
device.vru = state.vru;
|
||||
device.cheats = state.cheats;
|
||||
|
||||
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
|
||||
for (i, item) in tpak_rom.iter_mut().enumerate() {
|
||||
*item = device.transferpaks[i].cart.rom.clone();
|
||||
let state_data = if rewind {
|
||||
if let Some(rewind_frame) = rewind_frame {
|
||||
let timeout = std::time::Duration::from_secs(1);
|
||||
let now = std::time::Instant::now();
|
||||
loop {
|
||||
if let Some(state) = device
|
||||
.savestate
|
||||
.rewind_pool
|
||||
.blocking_lock()
|
||||
.remove(&rewind_frame)
|
||||
{
|
||||
break Some(state);
|
||||
}
|
||||
if now.elapsed() > timeout {
|
||||
break None;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
device.transferpaks = state.transferpaks;
|
||||
for (i, item) in tpak_rom.iter().enumerate() {
|
||||
device.transferpaks[i].cart.rom = item.clone();
|
||||
} else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last()
|
||||
{
|
||||
Some(state)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Ok(savestate) = std::fs::read(&device.ui.storage.paths.savestate_file_path)
|
||||
&& let Ok(device_data) = ui::storage::decompress_file(&savestate, "device")
|
||||
&& let Ok(saves_data) = ui::storage::decompress_file(&savestate, "saves")
|
||||
&& let Ok(rdp_state) = ui::storage::decompress_file(&savestate, "rdp_state")
|
||||
&& let Ok(ra_state) = ui::storage::decompress_file(&savestate, "ra_state")
|
||||
&& let Ok(state) = postcard::from_bytes::<Box<device::Device>>(&device_data)
|
||||
&& let Ok(saves) = postcard::from_bytes(&saves_data)
|
||||
{
|
||||
Some(SavestateData {
|
||||
device: state,
|
||||
saves,
|
||||
rdp_state,
|
||||
ra_state,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(mut state) = state_data
|
||||
&& device.rdram.size == state.device.rdram.size
|
||||
{
|
||||
if device.netplay.is_none() {
|
||||
ui::video::idle();
|
||||
}
|
||||
|
||||
device.savestate.last_rewind_saved = state.device.vi.elapsed_time;
|
||||
|
||||
std::mem::swap(&mut device.rng, &mut state.device.rng);
|
||||
|
||||
std::mem::swap(&mut device.ui.storage.saves, &mut state.saves);
|
||||
|
||||
std::mem::swap(&mut device.cpu, &mut state.device.cpu);
|
||||
std::mem::swap(&mut device.pif, &mut state.device.pif);
|
||||
|
||||
std::mem::swap(&mut device.cart, &mut state.device.cart);
|
||||
std::mem::swap(&mut device.cart.rom, &mut state.device.cart.rom); // ROM is not included in the savestate
|
||||
|
||||
std::mem::swap(&mut device.memory, &mut state.device.memory);
|
||||
std::mem::swap(&mut device.rsp, &mut state.device.rsp);
|
||||
std::mem::swap(&mut device.rdp, &mut state.device.rdp);
|
||||
|
||||
device.rdram.mem.copy_from_slice(&state.device.rdram.mem); // RDRAM address should not change
|
||||
std::mem::swap(&mut device.rdram.regs, &mut state.device.rdram.regs);
|
||||
|
||||
std::mem::swap(&mut device.mi, &mut state.device.mi);
|
||||
std::mem::swap(&mut device.pi, &mut state.device.pi);
|
||||
std::mem::swap(&mut device.vi, &mut state.device.vi);
|
||||
std::mem::swap(&mut device.ai, &mut state.device.ai);
|
||||
std::mem::swap(&mut device.si, &mut state.device.si);
|
||||
std::mem::swap(&mut device.ri, &mut state.device.ri);
|
||||
std::mem::swap(&mut device.vru, &mut state.device.vru);
|
||||
std::mem::swap(&mut device.cheats, &mut state.device.cheats);
|
||||
|
||||
std::mem::swap(&mut device.transferpaks, &mut state.device.transferpaks);
|
||||
for (i, item) in device.transferpaks.iter_mut().enumerate() {
|
||||
std::mem::swap(
|
||||
&mut item.cart.rom,
|
||||
&mut state.device.transferpaks[i].cart.rom,
|
||||
); // ROM is not included in the savestate
|
||||
}
|
||||
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
for line_index in 0..512 {
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].instruction[i] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i]);
|
||||
}
|
||||
}
|
||||
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
for line_index in 0..512 {
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].instruction[i] = device::cpu::decode_opcode(
|
||||
device,
|
||||
device.memory.icache[line_index].words[i],
|
||||
);
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if let Some(handler) = device.pif.channels[i].pak_handler {
|
||||
if handler.pak_type == device::controller::PakType::RumblePak {
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::MemPak {
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::TransferPak {
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if let Some(handler) = device.pif.channels[i].pak_handler {
|
||||
if handler.pak_type == device::controller::PakType::RumblePak {
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::MemPak {
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::TransferPak {
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui::audio::update_freq(device);
|
||||
ui::video::load_state(device, state.rdp_state.as_ptr());
|
||||
|
||||
ui::audio::close_game_audio(&mut device.ui);
|
||||
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
|
||||
ui::video::load_state(device, rdp_state.as_ptr());
|
||||
if !state.ra_state.is_empty() {
|
||||
retroachievements::load_state(state.ra_state.as_ptr(), state.ra_state.len());
|
||||
} else {
|
||||
retroachievements::load_state(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
if !ra_state.is_empty() {
|
||||
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
|
||||
} else {
|
||||
retroachievements::load_state(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
|
||||
if !rewind {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate loaded from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Failed to load savestate from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
false,
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Could not find savestate in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
false,
|
||||
);
|
||||
let (message, length) = if !rewind {
|
||||
(
|
||||
format!(
|
||||
"Failed to load savestate from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
)
|
||||
} else if device.netplay.is_none() {
|
||||
(
|
||||
"Failed to rewind".to_string(),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Failed to rollback".to_string(),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
)
|
||||
};
|
||||
ui::video::onscreen_message(&message, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android;
|
||||
pub mod audio;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod cheats;
|
||||
@@ -5,6 +7,7 @@ pub mod config;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod gui;
|
||||
pub mod input;
|
||||
pub mod input_profile;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod netplay;
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -12,26 +15,48 @@ pub mod retroachievements;
|
||||
pub mod storage;
|
||||
pub mod usb;
|
||||
pub mod video;
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
pub mod vru;
|
||||
|
||||
pub const APP_ID: &str = "io.github.gopher64.gopher64";
|
||||
|
||||
pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(format!(
|
||||
"{}/{}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("GIT_DESCRIBE")
|
||||
))
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Dirs {
|
||||
pub config_dir: std::path::PathBuf,
|
||||
pub data_dir: std::path::PathBuf,
|
||||
pub cache_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
unsafe impl Send for Audio {}
|
||||
unsafe impl Sync for Audio {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
pub joysticks: Vec<sdl3_sys::joystick::SDL_JoystickID>,
|
||||
pub keyboard_state: *const bool,
|
||||
pub controllers: [input::Controllers; 4],
|
||||
}
|
||||
|
||||
unsafe impl Send for Input {}
|
||||
unsafe impl Sync for Input {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Storage {
|
||||
pub save_type: Vec<storage::SaveTypes>,
|
||||
pub paths: storage::Paths,
|
||||
@@ -39,34 +64,40 @@ pub struct Storage {
|
||||
pub save_state_slot: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Video {
|
||||
pub window: *mut sdl3_sys::video::SDL_Window,
|
||||
pub fullscreen: bool,
|
||||
pub fps_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
pub vis_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
pub widescreen: bool,
|
||||
pub fps_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
|
||||
pub fps_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
|
||||
pub vis_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
|
||||
pub vis_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Video {}
|
||||
unsafe impl Sync for Video {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Usb {
|
||||
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
|
||||
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
|
||||
pub usb_tx: Option<tokio::sync::mpsc::UnboundedSender<usb::UsbData>>,
|
||||
pub cart_rx: Option<tokio::sync::mpsc::UnboundedReceiver<usb::UsbData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameSettings {
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub cheats: std::collections::HashMap<String, Option<String>>,
|
||||
pub cheats: rustc_hash::FxHashMap<String, Option<String>>,
|
||||
pub load_savestate_slot: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ui {
|
||||
pub dirs: Dirs,
|
||||
pub config: config::Config,
|
||||
pub game_id: String,
|
||||
pub game_hash: String,
|
||||
pub with_sdl: bool,
|
||||
pub audio: Audio,
|
||||
pub input: Input,
|
||||
pub storage: Storage,
|
||||
@@ -74,27 +105,34 @@ pub struct Ui {
|
||||
pub usb: Usb,
|
||||
}
|
||||
|
||||
impl Drop for Ui {
|
||||
fn drop(&mut self) {
|
||||
if self.with_sdl {
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_Quit();
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
pub fn sdl_hints() {
|
||||
unsafe {
|
||||
let hint = std::ffi::CString::new("1").unwrap();
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
|
||||
hint.as_ptr(),
|
||||
);
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY,
|
||||
hint.as_ptr(),
|
||||
);
|
||||
let app_id = std::ffi::CString::new(APP_ID).unwrap();
|
||||
sdl3_sys::everything::SDL_SetHint(sdl3_sys::everything::SDL_HINT_APP_ID, app_id.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
|
||||
unsafe {
|
||||
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
|
||||
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let err = sdl3_sys::error::SDL_GetError();
|
||||
panic!(
|
||||
"Could not initialize SDL subsystem: {}, {}",
|
||||
u32::from(flag),
|
||||
err
|
||||
if err.is_null() {
|
||||
"Unknown error"
|
||||
} else {
|
||||
std::ffi::CStr::from_ptr(err).to_str().unwrap()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -108,31 +146,44 @@ pub fn ttf_init() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sdl_close() {
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_Quit();
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dirs() -> Dirs {
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
let portable_dir = exe_path.parent();
|
||||
let portable = portable_dir.unwrap().join("portable.txt").exists();
|
||||
if portable {
|
||||
Dirs {
|
||||
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
|
||||
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
|
||||
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
|
||||
}
|
||||
} else {
|
||||
Dirs {
|
||||
config_dir: dirs::config_dir().unwrap().join("gopher64"),
|
||||
data_dir: dirs::data_dir().unwrap().join("gopher64"),
|
||||
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
|
||||
#[cfg(target_os = "android")]
|
||||
return android::get_dirs();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
let portable_dir = exe_path.parent();
|
||||
let portable = portable_dir.unwrap().join("portable.txt").exists();
|
||||
if portable {
|
||||
Dirs {
|
||||
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
|
||||
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
|
||||
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
|
||||
}
|
||||
} else {
|
||||
Dirs {
|
||||
config_dir: dirs::config_dir().unwrap().join("gopher64"),
|
||||
data_dir: dirs::data_dir().unwrap().join("gopher64"),
|
||||
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
fn construct_ui(joysticks: Vec<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> Ui {
|
||||
pub fn new() -> Ui {
|
||||
let dirs = get_dirs();
|
||||
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
|
||||
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (vis_tx, vis_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
Ui {
|
||||
input: Input {
|
||||
controllers: [
|
||||
@@ -140,25 +191,32 @@ impl Ui {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
],
|
||||
keyboard_state: std::ptr::null_mut(),
|
||||
joysticks,
|
||||
},
|
||||
storage: Storage {
|
||||
save_state_slot: 0,
|
||||
@@ -195,7 +253,7 @@ impl Ui {
|
||||
write_pending: false,
|
||||
},
|
||||
romsave: storage::RomSave {
|
||||
data: std::collections::HashMap::new(),
|
||||
data: rustc_hash::FxHashMap::default(),
|
||||
write_pending: false,
|
||||
},
|
||||
},
|
||||
@@ -210,9 +268,10 @@ impl Ui {
|
||||
video: Video {
|
||||
window: std::ptr::null_mut(),
|
||||
fullscreen: false,
|
||||
fps_tx,
|
||||
widescreen: false,
|
||||
fps_tx: Some(fps_tx),
|
||||
fps_rx: Some(fps_rx),
|
||||
vis_tx,
|
||||
vis_tx: Some(vis_tx),
|
||||
vis_rx: Some(vis_rx),
|
||||
},
|
||||
usb: Usb {
|
||||
@@ -220,26 +279,6 @@ impl Ui {
|
||||
cart_rx: None,
|
||||
},
|
||||
dirs,
|
||||
with_sdl,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Ui {
|
||||
Self::construct_ui(vec![], false)
|
||||
}
|
||||
|
||||
pub fn new() -> Ui {
|
||||
sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
let mut num_joysticks = 0;
|
||||
let joysticks = unsafe { sdl3_sys::joystick::SDL_GetJoysticks(&mut num_joysticks) };
|
||||
if joysticks.is_null() {
|
||||
panic!("Could not get joystick list");
|
||||
}
|
||||
let mut joystick_vec = vec![];
|
||||
for i in 0..num_joysticks {
|
||||
joystick_vec.push(unsafe { *joysticks.add(i as usize) });
|
||||
}
|
||||
unsafe { sdl3_sys::stdinc::SDL_free(joysticks as *mut std::ffi::c_void) }
|
||||
Self::construct_ui(joystick_vec, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
use crate::Args;
|
||||
use crate::create_runtime;
|
||||
use crate::run;
|
||||
use crate::ui;
|
||||
use clap::Parser;
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::refs::Global;
|
||||
use jni::sys::jint;
|
||||
use jni::{Env, EnvUnowned, JavaVM, bind_java_type};
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
const REQUEST_SELECT_ROM: jint = 1;
|
||||
const CONFIGURE_INPUT_PROFILE: jint = 2;
|
||||
const RUN_ROM: jint = 3;
|
||||
|
||||
pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
pub static SELECT_ROM_TX: tokio::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<Option<std::path::PathBuf>>>,
|
||||
> = tokio::sync::Mutex::const_new(None);
|
||||
|
||||
pub static WEAK_SLINT_WINDOW: std::sync::Mutex<Option<slint::Weak<ui::gui::AppWindow>>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
bind_java_type! {
|
||||
DocumentsContract => "android.provider.DocumentsContract",
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static EXTRA_INITIAL_URI: JString,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidActivity => "android.app.Activity",
|
||||
type_map = {
|
||||
AndroidIntent => "android.content.Intent",
|
||||
AndroidContentResolver => "android.content.ContentResolver",
|
||||
},
|
||||
methods {
|
||||
fn start_activity(intent: AndroidIntent) -> (),
|
||||
fn start_activity_for_result(intent: AndroidIntent, request_code: jint) -> (),
|
||||
fn get_content_resolver() -> AndroidContentResolver,
|
||||
},
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static RESULT_OK: jint,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidIntent => "android.content.Intent",
|
||||
type_map = {
|
||||
AndroidUri => "android.net.Uri",
|
||||
},
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static ACTION_VIEW: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static ACTION_OPEN_DOCUMENT: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static CATEGORY_OPENABLE: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static FLAG_ACTIVITY_NEW_TASK: jint,
|
||||
#[allow(non_snake_case)]
|
||||
static FLAG_GRANT_READ_URI_PERMISSION: jint,
|
||||
},
|
||||
constructors {
|
||||
fn new(),
|
||||
fn with_action(action: JString),
|
||||
},
|
||||
methods {
|
||||
fn set_data(uri: AndroidUri) -> AndroidIntent,
|
||||
fn set_type(r#type: JString) -> AndroidIntent,
|
||||
fn add_category(category: JString) -> AndroidIntent,
|
||||
fn get_data() -> AndroidUri,
|
||||
fn put_extra_string {
|
||||
sig = (extra: JString, value: JString) -> AndroidIntent,
|
||||
name = "putExtra",
|
||||
},
|
||||
fn put_extra_string_array {
|
||||
sig = (extra: JString, value: JString[]) -> AndroidIntent,
|
||||
name = "putExtra",
|
||||
},
|
||||
fn get_string_extra(name: JString) -> JString,
|
||||
fn set_class_name(package_name: JString, class_name: JString) -> AndroidIntent,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
|
||||
methods {
|
||||
fn close() -> (),
|
||||
fn detach_fd() -> jint,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidContentResolver => "android.content.ContentResolver",
|
||||
type_map = {
|
||||
AndroidUri => "android.net.Uri",
|
||||
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
|
||||
},
|
||||
methods {
|
||||
fn take_persistable_uri_permission(uri: AndroidUri, flags: jint) -> (),
|
||||
fn open_file_descriptor(uri: AndroidUri, mode: JString) -> ParcelFileDescriptor,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidUri => "android.net.Uri",
|
||||
methods {
|
||||
static fn parse(uri_string: JString) -> AndroidUri,
|
||||
fn to_string() -> JString,
|
||||
static fn decode(path: JString) -> JString,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
pub AndroidInputDevice => "android.view.InputDevice",
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static SOURCE_JOYSTICK: jint,
|
||||
#[allow(non_snake_case)]
|
||||
static SOURCE_GAMEPAD: jint,
|
||||
},
|
||||
methods {
|
||||
static fn get_device_ids() -> jint[],
|
||||
static fn get_device(device_id: jint) -> AndroidInputDevice,
|
||||
fn supports_source(source: jint) -> jboolean,
|
||||
fn is_virtual() -> jboolean,
|
||||
fn is_external() -> jboolean,
|
||||
fn get_vendor_id() -> jint,
|
||||
fn get_product_id() -> jint,
|
||||
fn get_name() -> JString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ControllerInfo {
|
||||
pub name: String,
|
||||
/// Stable ID from [`InputDevice.getDescriptor`](https://developer.android.com/reference/android/view/InputDevice#getDescriptor()).
|
||||
pub descriptor: String,
|
||||
}
|
||||
|
||||
fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> Vec<String> {
|
||||
if argc <= 0 || argv.is_null() {
|
||||
return Vec::new();
|
||||
}
|
||||
unsafe {
|
||||
(0..argc as usize)
|
||||
.map(|i| {
|
||||
std::ffi::CStr::from_ptr(*argv.add(i))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn gopher64_sdl_main(
|
||||
argc: std::ffi::c_int,
|
||||
argv: *mut *mut std::ffi::c_char,
|
||||
) -> std::ffi::c_int {
|
||||
let (close_tx, handle) = create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
let raw = argv_to_strings(argc, argv);
|
||||
let args = Args::try_parse_from(raw).unwrap();
|
||||
if let Err(err) = run(args, argc as usize) {
|
||||
close_tx.send(()).unwrap();
|
||||
eprintln!("Error running game: {err:?}");
|
||||
return 1;
|
||||
}
|
||||
close_tx.send(()).unwrap();
|
||||
0
|
||||
}
|
||||
|
||||
pub fn spawn_configure_input_profile(
|
||||
profile_name: slint::SharedString,
|
||||
dinput: bool,
|
||||
deadzone: i32,
|
||||
) {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| {
|
||||
start_configure_input_profile_on_jvm(
|
||||
env,
|
||||
app,
|
||||
profile_name.to_string(),
|
||||
dinput,
|
||||
deadzone,
|
||||
)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_configure_input_profile_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
profile_name: String,
|
||||
dinput: bool,
|
||||
deadzone: i32,
|
||||
) -> jni::errors::Result<()> {
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
|
||||
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
|
||||
|
||||
let args_key = JString::from_str(env, "args")?;
|
||||
let mut args = vec![
|
||||
JString::from_str(env, "--configure-input-profile")?,
|
||||
JString::from_str(env, &profile_name)?,
|
||||
JString::from_str(env, "--deadzone")?,
|
||||
JString::from_str(env, &deadzone.to_string())?,
|
||||
];
|
||||
if dinput {
|
||||
args.push(JString::from_str(env, "--use-dinput")?);
|
||||
}
|
||||
let empty_arg = JString::from_str(env, "")?;
|
||||
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
j_args.set_element(env, i, arg)?;
|
||||
}
|
||||
let intent = AndroidIntent::new(env)?
|
||||
.set_class_name(env, &package_name, &class_name)?
|
||||
.put_extra_string_array(env, &args_key, &j_args)?;
|
||||
|
||||
activity
|
||||
.as_ref()
|
||||
.start_activity_for_result(env, &intent, CONFIGURE_INPUT_PROFILE)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<ui::gui::NetplayDevice>,
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| {
|
||||
start_run_rom_on_jvm(env, app, file_path, game_settings, netplay, weak)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_run_rom_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<ui::gui::NetplayDevice>,
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) -> jni::errors::Result<()> {
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
|
||||
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
|
||||
|
||||
let file_path_key = JString::from_str(env, "file_path")?;
|
||||
let file_path = file_path.to_str().unwrap();
|
||||
let cheats_path_key = JString::from_str(env, "cheats_path")?;
|
||||
let cheats_path = app
|
||||
.internal_data_path()
|
||||
.unwrap()
|
||||
.join("cache")
|
||||
.join("cheats.json");
|
||||
|
||||
let args_key = JString::from_str(env, "args")?;
|
||||
let mut args = vec![
|
||||
JString::from_str(env, file_path)?,
|
||||
JString::from_str(env, "--fullscreen")?,
|
||||
JString::from_str(env, "--overclock")?,
|
||||
JString::from_str(env, &game_settings.overclock.to_string())?,
|
||||
JString::from_str(env, "--disable-expansion-pak")?,
|
||||
JString::from_str(env, &game_settings.disable_expansion_pak.to_string())?,
|
||||
];
|
||||
|
||||
if let Some(netplay) = netplay {
|
||||
args.push(JString::from_str(env, "--netplay-server-addr")?);
|
||||
args.push(JString::from_str(env, &netplay.server_addr)?);
|
||||
args.push(JString::from_str(env, "--netplay-player-number")?);
|
||||
args.push(JString::from_str(env, &netplay.player_number.to_string())?);
|
||||
args.push(JString::from_str(env, "--netplay-number-of-players")?);
|
||||
args.push(JString::from_str(
|
||||
env,
|
||||
&netplay.number_of_players.to_string(),
|
||||
)?);
|
||||
args.push(JString::from_str(env, "--netplay-input-delay")?);
|
||||
args.push(JString::from_str(env, &netplay.input_delay.to_string())?);
|
||||
args.push(JString::from_str(env, "--cheats")?);
|
||||
args.push(JString::from_str(env, cheats_path.to_str().unwrap())?);
|
||||
|
||||
let f = std::fs::File::create(&cheats_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
|
||||
}
|
||||
|
||||
let empty_arg = JString::from_str(env, "")?;
|
||||
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
j_args.set_element(env, i, arg)?;
|
||||
}
|
||||
|
||||
let file_path_string = JString::from_str(env, file_path)?;
|
||||
let cheats_path_string = JString::from_str(env, cheats_path.to_str().unwrap())?;
|
||||
let intent = AndroidIntent::new(env)?
|
||||
.set_class_name(env, &package_name, &class_name)?
|
||||
.put_extra_string(env, &file_path_key, &file_path_string)?
|
||||
.put_extra_string(env, &cheats_path_key, &cheats_path_string)?
|
||||
.put_extra_string_array(env, &args_key, &j_args)?;
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
|
||||
activity
|
||||
.as_ref()
|
||||
.start_activity_for_result(env, &intent, RUN_ROM)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_vm(app: &slint::android::AndroidApp) -> JavaVM {
|
||||
unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
|
||||
}
|
||||
|
||||
pub fn decode_path(path: &str) -> String {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
match get_vm(app).attach_current_thread(|env| decode_path_on_jvm(env, path)) {
|
||||
Ok(decoded_path) => decoded_path,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while decoding path: {err:?}");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_path_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<String> {
|
||||
let path = JString::from_str(env, path)?;
|
||||
let decoded_path = AndroidUri::decode(env, &path)?;
|
||||
Ok(decoded_path.try_to_string(env)?)
|
||||
}
|
||||
|
||||
/// Lists connected gamepads and joysticks using the Android framework.
|
||||
pub fn list_controllers() -> Vec<ControllerInfo> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
match get_vm(app).attach_current_thread(list_controllers_on_jvm) {
|
||||
Ok(controllers) => controllers,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while listing controllers: {err:?}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<ControllerInfo>> {
|
||||
let device_ids = AndroidInputDevice::get_device_ids(env)?;
|
||||
let count = device_ids.len(env)?;
|
||||
let mut ids = vec![0i32; count];
|
||||
if count > 0 {
|
||||
device_ids.get_region(env, 0, &mut ids)?;
|
||||
}
|
||||
|
||||
let mut controllers = Vec::new();
|
||||
for device_id in ids {
|
||||
let device = AndroidInputDevice::get_device(env, device_id)?;
|
||||
if device.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let supports_gamepad =
|
||||
device.supports_source(env, AndroidInputDevice::SOURCE_GAMEPAD(env)?)?;
|
||||
let supports_joystick =
|
||||
device.supports_source(env, AndroidInputDevice::SOURCE_JOYSTICK(env)?)?;
|
||||
if !supports_gamepad && !supports_joystick {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = if let Ok(name) = device.get_name(env)
|
||||
&& let Ok(name) = name.try_to_string(env)
|
||||
{
|
||||
name
|
||||
} else {
|
||||
ui::input::UNKNOWN_CONTROLLER_NAME.to_string()
|
||||
};
|
||||
let descriptor = if let Ok(product_id) = device.get_product_id(env)
|
||||
&& let Ok(vendor_id) = device.get_vendor_id(env)
|
||||
{
|
||||
format!("{}:{}:{}", name, vendor_id, product_id)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
controllers.push(ControllerInfo { name, descriptor });
|
||||
}
|
||||
|
||||
Ok(controllers)
|
||||
}
|
||||
|
||||
/// Opens a URI in the user's default app via [`Intent::ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW).
|
||||
pub fn open_uri(path: &str) {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let path = path.to_string();
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| open_uri_on_jvm(env, app, &path))
|
||||
{
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_uri_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
path: &str,
|
||||
) -> jni::errors::Result<()> {
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let uri_string = JString::from_str(env, path)?;
|
||||
let uri = AndroidUri::parse(env, &uri_string)?;
|
||||
|
||||
let action_view = AndroidIntent::ACTION_VIEW(env)?;
|
||||
let intent = AndroidIntent::with_action(env, &action_view)?.set_data(env, &uri)?;
|
||||
|
||||
activity.as_ref().start_activity(env, &intent)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Option<std::path::PathBuf>>();
|
||||
SELECT_ROM_TX.lock().await.replace(tx);
|
||||
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app)
|
||||
.attach_current_thread(|env| select_rom_on_jvm(env, app, rom_dir.to_string()))
|
||||
{
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
return None;
|
||||
}
|
||||
|
||||
rx.await.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub async fn select_gb_rom(_player: i32) -> Option<std::path::PathBuf> {
|
||||
select_rom(slint::SharedString::new()).await
|
||||
}
|
||||
|
||||
pub async fn select_gb_ram(_player: i32) -> Option<std::path::PathBuf> {
|
||||
select_rom(slint::SharedString::new()).await
|
||||
}
|
||||
|
||||
fn select_rom_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
rom_dir: String,
|
||||
) -> jni::errors::Result<()> {
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let action = AndroidIntent::ACTION_OPEN_DOCUMENT(env)?;
|
||||
let category = AndroidIntent::CATEGORY_OPENABLE(env)?;
|
||||
let mime_type = JString::from_str(env, "*/*")?;
|
||||
let mut intent = AndroidIntent::with_action(env, &action)?
|
||||
.set_type(env, &mime_type)?
|
||||
.add_category(env, &category)?;
|
||||
if !rom_dir.is_empty() {
|
||||
let start_dir = JString::from_str(env, rom_dir)?;
|
||||
let extra_initial_uri = DocumentsContract::EXTRA_INITIAL_URI(env)?;
|
||||
intent = intent.put_extra_string(env, &extra_initial_uri, &start_dir)?;
|
||||
}
|
||||
|
||||
activity.start_activity_for_result(env, &intent, REQUEST_SELECT_ROM)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_dirs() -> ui::Dirs {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
ui::Dirs {
|
||||
config_dir: app.internal_data_path().unwrap().join("config"),
|
||||
data_dir: app.external_data_path().unwrap().join("data"),
|
||||
cache_dir: app.internal_data_path().unwrap().join("cache"),
|
||||
}
|
||||
} else {
|
||||
panic!("Android app not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_controller_names() -> Vec<String> {
|
||||
let mut controllers: Vec<String> = list_controllers().into_iter().map(|c| c.name).collect();
|
||||
controllers.insert(0, "None".into());
|
||||
controllers
|
||||
}
|
||||
|
||||
pub fn get_controller_paths() -> Vec<String> {
|
||||
let mut controller_paths: Vec<String> = list_controllers()
|
||||
.into_iter()
|
||||
.map(|c| c.descriptor)
|
||||
.collect();
|
||||
controller_paths.insert(0, String::new());
|
||||
controller_paths
|
||||
}
|
||||
|
||||
pub fn rom_exists(path: &str) -> bool {
|
||||
get_file_from_uri(&std::path::PathBuf::from(path)).is_some()
|
||||
}
|
||||
|
||||
pub fn get_file_from_uri(path: &std::path::PathBuf) -> Option<std::fs::File> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let path = path.to_str().unwrap().into();
|
||||
match get_vm(app).attach_current_thread(|env| get_file_from_uri_on_jvm(env, app, path)) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_from_uri_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
path: String,
|
||||
) -> jni::errors::Result<Option<std::fs::File>> {
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
let path = JString::from_str(env, path)?;
|
||||
let mode = JString::from_str(env, "r")?;
|
||||
let uri = AndroidUri::parse(env, &path)?;
|
||||
|
||||
let content_resolver = activity.as_ref().get_content_resolver(env)?;
|
||||
let parcel_file_descriptor = content_resolver.open_file_descriptor(env, &uri, &mode);
|
||||
if let Ok(descriptor) = parcel_file_descriptor
|
||||
&& !descriptor.is_null()
|
||||
{
|
||||
let owned_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor.detach_fd(env)?) };
|
||||
let file = std::fs::File::from(owned_fd);
|
||||
descriptor.close(env)?;
|
||||
return Ok(Some(file));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnActivityResult<
|
||||
'caller,
|
||||
>(
|
||||
mut unowned_env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
request_code: jint,
|
||||
result_code: jint,
|
||||
intent_data: JObject<'caller>,
|
||||
) {
|
||||
let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> {
|
||||
if result_code == AndroidActivity::RESULT_OK(env)? {
|
||||
match request_code {
|
||||
REQUEST_SELECT_ROM => {
|
||||
if let Some(tx) = SELECT_ROM_TX.blocking_lock().take()
|
||||
&& !intent_data.is_null()
|
||||
{
|
||||
let result_intent =
|
||||
unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
|
||||
|
||||
let uri = result_intent.as_ref().get_data(env)?;
|
||||
if uri.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe {
|
||||
env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)?
|
||||
};
|
||||
|
||||
let content_resolver = activity.as_ref().get_content_resolver(env)?;
|
||||
let take_flags = AndroidIntent::FLAG_GRANT_READ_URI_PERMISSION(env)?;
|
||||
content_resolver
|
||||
.take_persistable_uri_permission(env, &uri, take_flags)?;
|
||||
|
||||
let path = uri.to_string(env)?;
|
||||
|
||||
let _ = tx.send(Some(std::path::PathBuf::from(path.to_string())));
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
CONFIGURE_INPUT_PROFILE => {
|
||||
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
let config = ui::config::Config::new();
|
||||
ui::gui::update_input_profiles(&weak_app_window, &config);
|
||||
}
|
||||
}
|
||||
RUN_ROM => {
|
||||
let result_intent = unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
|
||||
|
||||
let file_path_key = JString::from_str(env, "file_path")?;
|
||||
let file_path = result_intent
|
||||
.as_ref()
|
||||
.get_string_extra(env, &file_path_key)?
|
||||
.try_to_string(env)?;
|
||||
|
||||
let cheats_path_key = JString::from_str(env, "cheats_path")?;
|
||||
if let Ok(cheats_path) = result_intent
|
||||
.as_ref()
|
||||
.get_string_extra(env, &cheats_path_key)
|
||||
&& let Ok(cheats_path) = cheats_path.try_to_string(env)
|
||||
{
|
||||
let _ = std::fs::remove_file(cheats_path);
|
||||
}
|
||||
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
weak_app_window
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
ui::gui::update_recent_roms(&handle, file_path.into());
|
||||
handle.set_game_running(false)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
|
||||
}
|
||||
@@ -1,19 +1,62 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
|
||||
pub fn init(ui: &mut ui::Ui, frequency: u64) {
|
||||
static ADJUST_LOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
|
||||
init_game_audio(ui, frequency);
|
||||
init_game_audio(device);
|
||||
}
|
||||
|
||||
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
unsafe extern "C" fn audio_callback(
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
additional_amount: i32,
|
||||
_total_amount: i32,
|
||||
) {
|
||||
if additional_amount > 0 {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
audio_stream,
|
||||
vec![0; additional_amount as usize].as_ptr() as *const std::ffi::c_void,
|
||||
additional_amount,
|
||||
)
|
||||
};
|
||||
|
||||
adjust_audio_frequency(audio_stream, -0.0005);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_freq(device: &mut device::Device) {
|
||||
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: frequency as i32,
|
||||
freq: device.ai.freq as i32,
|
||||
channels: 2,
|
||||
};
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamFormat(
|
||||
device.ui.audio.audio_stream,
|
||||
&game_audio_spec,
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
if device.ai.regs[device::ai::AI_STATUS_REG] & device::ai::AI_STATUS_BUSY != 0 {
|
||||
resume_game_audio(&mut device.ui);
|
||||
} else {
|
||||
pause_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_game_audio(device: &mut device::Device) {
|
||||
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: device.ai.freq as i32,
|
||||
channels: 2,
|
||||
};
|
||||
|
||||
ui.audio.audio_stream = unsafe {
|
||||
device.ui.audio.audio_stream = unsafe {
|
||||
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&game_audio_spec,
|
||||
@@ -21,14 +64,23 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_stream.is_null() {
|
||||
if device.ui.audio.audio_stream.is_null() {
|
||||
panic!("Could not create audio stream");
|
||||
}
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(device.ui.audio.audio_stream, device.ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_SetAudioStreamGetCallback(
|
||||
device.ui.audio.audio_stream,
|
||||
Some(audio_callback),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
} {
|
||||
panic!("Could not resume audio stream");
|
||||
panic!("Could not initialize audio stream");
|
||||
}
|
||||
if device.ai.regs[device::ai::AI_STATUS_REG] & device::ai::AI_STATUS_BUSY != 0 {
|
||||
resume_game_audio(&mut device.ui);
|
||||
} else {
|
||||
pause_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +88,18 @@ pub fn close(ui: &mut ui::Ui) {
|
||||
close_game_audio(ui);
|
||||
}
|
||||
|
||||
pub fn resume_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_PauseAudioStreamDevice(ui.audio.audio_stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
if !ui.audio.audio_stream.is_null() {
|
||||
@@ -55,7 +119,7 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
|
||||
}
|
||||
ui::video::onscreen_message(
|
||||
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
|
||||
false,
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,26 +133,38 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
}
|
||||
ui::video::onscreen_message(
|
||||
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
|
||||
false,
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
|
||||
if !device.vi.enable_speed_limiter {
|
||||
unsafe extern "C" fn lock_callback(
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
_id: sdl3_sys::timer::SDL_TimerID,
|
||||
_interval: u32,
|
||||
) -> u32 {
|
||||
ADJUST_LOCKED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
0
|
||||
}
|
||||
|
||||
fn adjust_audio_frequency(audio_stream: *mut sdl3_sys::audio::SDL_AudioStream, frequency: f32) {
|
||||
if ADJUST_LOCKED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
ADJUST_LOCKED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
unsafe {
|
||||
let current_ratio =
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream);
|
||||
sdl3_sys::timer::SDL_AddTimer(1000, Some(lock_callback), std::ptr::null_mut());
|
||||
let current_ratio = sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream);
|
||||
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
|
||||
device.ui.audio.audio_stream,
|
||||
audio_stream,
|
||||
(current_ratio + frequency).clamp(0.995, 1.005),
|
||||
);
|
||||
|
||||
/*
|
||||
println!(
|
||||
"Adjusted audio frequency ratio to {}",
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream)
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream)
|
||||
);
|
||||
*/
|
||||
}
|
||||
@@ -99,6 +175,10 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
return;
|
||||
}
|
||||
|
||||
if netplay::in_rollback(device.netplay.as_ref()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut primary_buffer: Vec<i16> = vec![0; length as usize / 2];
|
||||
let mut i = 0;
|
||||
while i < length as usize / 2 {
|
||||
@@ -116,44 +196,15 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
|
||||
let samples_per_frame = device.ai.freq as f64 * device.vi.frame_time * 4.0;
|
||||
let max_latency = samples_per_frame * 8.0;
|
||||
|
||||
if audio_queued < samples_per_frame {
|
||||
let silence_buffer: Vec<u8> = vec![0; samples_per_frame as usize & !3];
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
device.ui.audio.audio_stream,
|
||||
silence_buffer.as_ptr() as *const std::ffi::c_void,
|
||||
silence_buffer.len() as i32,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
/*
|
||||
println!(
|
||||
"Audio underrun: queued {} samples, expected at least {} samples",
|
||||
audio_queued, samples_per_frame
|
||||
);
|
||||
*/
|
||||
adjust_audio_frequency(device, -0.0005);
|
||||
}
|
||||
|
||||
if audio_queued < max_latency {
|
||||
if !unsafe {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
device.ui.audio.audio_stream,
|
||||
primary_buffer.as_ptr() as *const std::ffi::c_void,
|
||||
primary_buffer.len() as i32 * 2,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
println!(
|
||||
"Audio overrun: queued {} samples, expected at most {} samples",
|
||||
audio_queued, max_latency
|
||||
);
|
||||
*/
|
||||
adjust_audio_frequency(device, 0.0005);
|
||||
};
|
||||
} else if device.speed_limiter.enabled {
|
||||
adjust_audio_frequency(device.ui.audio.audio_stream, 0.0005);
|
||||
}
|
||||
}
|
||||
|
||||