Compare commits

...

32 Commits

Author SHA1 Message Date
Mr-Wiseguy
fc9aad1fc3 Update RT64 to fix HD texture max mipmap selection issue 2024-06-19 23:06:36 -04:00
Mr-Wiseguy
b9b567e024 Fix texcoords for seamless pause background patch 2024-06-19 21:17:23 -04:00
Wiseguy
1978a72690 Remove STB implementation as it's already defined in RT64 2024-06-19 21:17:23 -04:00
Reonu
5a80cd028c WIP HD texture support 2024-06-19 21:17:23 -04:00
David Chavez
07cfe51010
Update SDL2 version for remaining workflows (#401) 2024-06-17 13:10:34 +02:00
David Chavez
b086945b67
Add Blaze message to README (#397) 2024-06-17 00:51:21 +02:00
David Chavez
1a6a3b3082
CI: build on ubuntu-18.04 (#346) 2024-06-16 21:38:23 +02:00
David Chavez
19fcd9bf31
Update to latest N64ModernRuntime (#392)
- updates to new controller changes
- updates to new render context changes
- updates to new controller number changes
- fix for crash on save thread
2024-06-12 09:40:14 +02:00
David Chavez
b31cf0a76b
Update N64ModernRuntime submodule url (#389) 2024-06-10 16:50:54 +02:00
David Chavez
0e31d3b582
CI: Match SDL across platforms and centralize versions (#383) 2024-06-09 01:42:15 +02:00
briaguya
2607c8098f
ci: download sdl from gh release url (#381) 2024-06-08 21:09:08 +02:00
David Chavez
e819c62da2
ci: fix windows builds (#375) 2024-06-07 10:39:47 +02:00
briaguya
b0d0cab6c2
feat: support portable mode (#368) 2024-06-06 01:59:43 +02:00
briaguya
1c00519938
fix: enable key repeat in rmlui menu (#364) 2024-06-05 13:03:06 +02:00
David Chavez
030d793056
Update to support latest N64Recomp - Silence warnings in CMake (#358) 2024-06-05 07:58:24 +02:00
David Chavez
bec699f0bd
Migrate to N64ModernRuntime (#354) 2024-06-05 01:12:43 +02:00
David Chavez
6e9ee3498b
CI: Pin N64Recomp version to use (#353) 2024-06-03 21:39:20 +02:00
Wiseguy
4d682fac8a
Bump version number to 1.1.1 (#343) 2024-06-02 16:29:13 -04:00
David Chavez
d1f5ff3bba
Use shader cache in artefacts (#272) 2024-06-02 14:18:50 -04:00
Wiseguy
9ab97993e1
Update RT64 for AMD RDNA3 workaround and shader counts in debugger (#314) 2024-06-02 13:47:03 -04:00
Mr-Wiseguy
49baa68ea1 Fix bug with saving error detection, changed saving error message 2024-06-02 12:01:57 -04:00
David Chavez
db1def4280
CI: Enable internal workflow for pushes on dev (#341) 2024-06-02 17:45:01 +02:00
Reonu
7d3ae05436
Add shield aiming inversion (#338) 2024-06-02 10:46:08 -04:00
David Chavez
763c714653
CI: Split workflow execution paths (#337) 2024-06-02 10:34:25 -04:00
Wiseguy
ce406e9c5d
Use file backup system for config files (#335)
* Use file backup system for config files

* Add -fexceptions to windows cxxflags in workflow
2024-06-01 17:22:33 -04:00
Reonu
3e5efc935e
Start the Wart boss fight if the player looks up with analog cam (#308) 2024-05-30 18:31:09 +01:00
thecozies
a3d287575d
Enhanced analog cam behavior when spike rolling or in a deku flower (#306)
* 279 Adjusted autocam/analog cam behavior while inside of a deku flower or when spike rolling

* override analog cam while left stick is active in deku flower

* Change to auto cam when spike rolling (#323)

* Fix duplicate skip_analog_cam_once declaration

---------

Co-authored-by: Reonu <danileon95@gmail.com>
2024-05-30 16:34:10 +01:00
Reonu
8319d97ad1
Make the ocarina work with the dpad (#311) 2024-05-30 15:53:43 +01:00
Reonu
b6b3bca731
Update issue template with questions for glitches and driver versions (#284) 2024-05-27 15:54:10 +01:00
David Chavez
95b7a64f1c
CI: Build aarch64 linux (#249) 2024-05-27 12:33:15 +02:00
Mr-Wiseguy
790b10a4b4 Fixed swapped descriptions in general config menu 2024-05-26 15:28:34 -04:00
Reonu
32b0fc5f53
Update README to reflect new features (dual analog etc) (#276) 2024-05-26 15:25:03 -04:00
85 changed files with 1803 additions and 9611 deletions

View File

@ -6,8 +6,15 @@ labels: ''
assignees: ''
---
## If you have a crash on startup, please make sure your graphics drivers are up to date before submitting a bug report.
**What is your GPU driver version? Old drivers, particularly on Nvidia, are known to cause crashes on boot on 1.1.0. If you are on Nvidia and the game is crashing on boot, please update to driver version 555.85 or newer before opening an issue.**
**Have you checked whether this issue is vanilla behavior? In other words, does it occur on original hardware?**
**Were you playing with intended mechanics, or using glitches? If it's the latter, which glitches?**
**Describe the bug**
A clear and concise description of what the bug is.
@ -27,6 +34,7 @@ Please attach a screenshot of the bug.
- Version: [e.g. 1.0.0]
- CPU: [e.g. Intel Core ..., AMD Ryzen ..., etc.]
- GPU: [e.g. NVIDIA GeForce .../Radeon RX .../Intel UHD .../etc.]
- GPU driver: [e.g Nvidia driver 545.XX, AMD driver 24.X.X, etc]
**Additional context**
Add any other context about the problem here.

View File

@ -1,17 +1,45 @@
curl -sSfLO "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage"
ARCH=$(uname -m)
LINUX_DEPLOY_ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
ARCH="x86_64"
LINUX_DEPLOY_ARCH="x86_64"
elif [ "$ARCH" = "aarch64" ]; then
ARCH="arm_aarch64"
LINUX_DEPLOY_ARCH="aarch64"
else
echo "Unsupported architecture: $ARCH"
exit 1
fi
curl -sSfLO "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$LINUX_DEPLOY_ARCH.AppImage"
curl -sSfLO "https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/raw/master/linuxdeploy-plugin-gtk.sh"
chmod a+x linuxdeploy*
mkdir -p AppDir/usr/bin
cp Zelda64Recompiled AppDir/usr/bin/
cp -r assets/ AppDir/usr/bin/
cp gamecontrollerdb.txt AppDir/usr/bin/
cp icons/512.png AppDir/Zelda64Recompiled.png
cp .github/linux/Zelda64Recompiled.desktop AppDir/
./linuxdeploy-static-x86_64.AppImage --appimage-extract
"./linuxdeploy-$LINUX_DEPLOY_ARCH.AppImage" --appimage-extract
mv squashfs-root/ deploy
ARCH=x86_64 ./deploy/AppRun --appdir=AppDir/ -d AppDir/Zelda64Recompiled.desktop -i AppDir/Zelda64Recompiled.png -e AppDir/usr/bin/Zelda64Recompiled --plugin gtk
./deploy/AppRun --appdir=AppDir/ -d AppDir/Zelda64Recompiled.desktop -i AppDir/Zelda64Recompiled.png -e AppDir/usr/bin/Zelda64Recompiled --plugin gtk
sed -i 's/exec/#exec/g' AppDir/AppRun
echo 'cd "$this_dir"/usr/bin/' >> AppDir/AppRun
echo './Zelda64Recompiled' >> AppDir/AppRun
ARCH=x86_64 ./deploy/usr/bin/linuxdeploy-plugin-appimage --appdir=AppDir
echo 'if [ -f "portable.txt" ]; then' >> AppDir/AppRun
echo ' APP_FOLDER_PATH=$PWD' >> AppDir/AppRun
echo ' cd "$this_dir"/usr/bin/' >> AppDir/AppRun
echo ' APP_FOLDER_PATH=$APP_FOLDER_PATH ./Zelda64Recompiled' >> AppDir/AppRun
echo 'else' >> AppDir/AppRun
echo ' cd "$this_dir"/usr/bin/' >> AppDir/AppRun
echo ' ./Zelda64Recompiled' >> AppDir/AppRun
echo 'fi' >> AppDir/AppRun
# Remove conflicting libraries
rm -rf AppDir/usr/lib/libgmodule*
rm -rf AppDir/usr/lib/gio/modules/*.so
rm -rf AppDir/usr/lib/libwayland*
./deploy/usr/bin/linuxdeploy-plugin-appimage --appdir=AppDir

View File

@ -1,28 +1,111 @@
name: validate
on:
push:
branches:
- dev
pull_request_target:
types: [opened, synchronize]
workflow_call:
inputs:
SDL2_VERSION:
type: string
required: false
default: '2.30.3'
N64RECOMP_COMMIT:
type: string
required: false
default: '2a2df89349ff25a3afb3a09617deb3a166efe2f3'
DXC_CHECKSUM:
type: string
required: false
default: '4e6f4e52989aca69739880b40b9f988357f15d10ca03284377b81f1502463ff5'
secrets:
ZRE_REPO_WITH_PAT:
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
authorize:
environment:
${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo ✓
build-unix:
needs: authorize
runs-on: ubuntu-22.04
build-linux:
runs-on: ${{ matrix.os }}
container:
image: dcvz/n64recomp:0.0.1-ubuntu-18.04
strategy:
matrix:
type: [ Debug, Release ]
os: [ ubuntu-22.04 ]
name: ubuntu-18.04 (x64, ${{ matrix.type }})
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
submodules: recursive
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ matrix.os }}-z64re-ccache-${{ matrix.type }}-x64-${{ inputs.N64RECOMP_COMMIT }}
- name: Prepare Build
run: |-
git clone ${{ secrets.ZRE_REPO_WITH_PAT }}
unzip zre/files.zip > /dev/null 2>&1
- name: Build N64Recomp & RSPRecomp
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout ${{ inputs.N64RECOMP_COMMIT }}
git submodule update --init --recursive
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
# Build N64Recomp & RSPRecomp
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config Release --target N64Recomp -j $(nproc)
cmake --build cmake-build --config Release --target RSPRecomp -j $(nproc)
# Copy N64Recomp & RSPRecomp to root directory
cp cmake-build/N64Recomp ..
cp cmake-build/RSPRecomp ..
- name: Run N64Recomp & RSPRecomp
run: |
./N64Recomp us.rev1.toml
./RSPRecomp aspMain.us.rev1.toml
./RSPRecomp njpgdspMain.us.rev1.toml
- name: Hotpatch DXC into RT64's contrib
run: |
# check if dxc was updated before we replace it, to detect changes
echo ${{ inputs.DXC_CHECKSUM }} ./lib/rt64/src/contrib/dxc/bin/x64/dxc | sha256sum --status -c -
cp -v /usr/local/lib/libdxcompiler.so ./lib/rt64/src/contrib/dxc/lib/x64/libdxcompiler.so
cp -v /usr/local/bin/dxc ./lib/rt64/src/contrib/dxc/bin/x64/dxc
- name: Build ZeldaRecomp
run: |-
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang++-17 -DCMAKE_C_COMPILER=clang-17 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DPATCHES_C_COMPILER=clang-17 -DPATCHES_LD=ld.lld-17 -DPATCHES_OBJCOPY=llvm-objcopy-17
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $(nproc)
- name: Prepare Archive
run: |
mv cmake-build/Zelda64Recompiled Zelda64Recompiled
rm -rf assets/scss
tar -czf Zelda64Recompiled.tar.gz Zelda64Recompiled assets/ gamecontrollerdb.txt
- name: Archive Zelda64Recomp
uses: actions/upload-artifact@v3
with:
name: Zelda64Recompiled-${{ runner.os }}-X64-${{ matrix.type }}
path: Zelda64Recompiled.tar.gz
- name: Build AppImage
run: |-
./.github/linux/appimage.sh
- name: Zelda64Recomp AppImage
uses: actions/upload-artifact@v3
with:
name: Zelda64Recompiled-AppImage-X64-${{ matrix.type }}
path: Zelda64Recompiled-*.AppImage
build-linux-arm64:
runs-on: ${{ format('blaze/{0}', matrix.os) }}
strategy:
matrix:
type: [ Debug, Release ]
os: [ ubuntu-22.04 ]
name: ${{ matrix.os }} (arm64, ${{ matrix.type }})
steps:
- name: Checkout
uses: actions/checkout@v4
@ -32,9 +115,8 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }}
key: ${{ matrix.os }}-z64re-ccache-${{ matrix.type }}-arm64-${{ inputs.N64RECOMP_COMMIT }}
- name: Install Linux Dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libsdl2-dev libgtk-3-dev lld llvm clang-15 libfuse2
@ -45,31 +127,33 @@ jobs:
# Enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
wget https://www.libsdl.org/release/SDL2-2.26.1.tar.gz
tar -xzf SDL2-2.26.1.tar.gz
cd SDL2-2.26.1
wget https://github.com/libsdl-org/SDL/releases/download/release-${{ inputs.SDL2_VERSION }}/SDL2-${{ inputs.SDL2_VERSION }}.tar.gz
tar -xzf SDL2-${{ inputs.SDL2_VERSION }}.tar.gz
cd SDL2-${{ inputs.SDL2_VERSION }}
./configure
make -j 10
sudo make install
sudo cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/
sudo cp -av /usr/local/lib/libSDL* /lib/aarch64-linux-gnu/
echo ::endgroup::
- name: Prepare Build
run: |-
git clone ${{ secrets.ZRE_REPO_WITH_PAT }}
./zre/process.sh
cp ./zre/mm_shader_cache.bin ./shadercache/
- name: Build N64Recomp & RSPRecomp
if: runner.os != 'Windows'
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout ${{ inputs.N64RECOMP_COMMIT }}
git submodule update --init --recursive
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
# Build N64Recomp & RSPRecomp
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config Release --target N64Recomp -j 8
cmake --build cmake-build --config Release --target RSPRecomp -j 8
cmake --build cmake-build --config Release --target N64Recomp -j $(nproc)
cmake --build cmake-build --config Release --target RSPRecomp -j $(nproc)
# Copy N64Recomp & RSPRecomp to root directory
cp cmake-build/N64Recomp ..
@ -85,30 +169,30 @@ jobs:
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang++-15 -DCMAKE_C_COMPILER=clang-15 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j 8
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $(nproc)
- name: Prepare Archive
run: |
mv cmake-build/Zelda64Recompiled Zelda64Recompiled
rm -rf assets/scss
tar -czf Zelda64Recompiled-${{ runner.os }}-${{ matrix.type }}.tar.gz Zelda64Recompiled assets/
tar -czf Zelda64Recompiled.tar.gz Zelda64Recompiled assets/ gamecontrollerdb.txt
- name: Archive Zelda64Recomp
uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-${{ runner.os }}-${{ matrix.type }}
path: Zelda64Recompiled-${{ runner.os }}-${{ matrix.type }}.tar.gz
name: Zelda64Recompiled-${{ runner.os }}-ARM64-${{ matrix.type }}
path: Zelda64Recompiled.tar.gz
- name: Prepare AppImage
run: ./.github/linux/appimage.sh
- name: Zelda64Recomp AppImage
uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-AppImage-${{ matrix.type }}
path: Zelda64Recompiled-x86_64.AppImage
name: Zelda64Recompiled-AppImage-ARM64-${{ matrix.type }}
path: Zelda64Recompiled-*.AppImage
build-windows:
needs: authorize
runs-on: windows-latest
strategy:
matrix:
type: [ Debug, Release ]
name: windows (${{ matrix.type }})
steps:
- name: Checkout
uses: actions/checkout@v4
@ -129,17 +213,21 @@ jobs:
run: |-
git clone ${{ secrets.ZRE_REPO_WITH_PAT }}
./zre/process.ps1
cp ./zre/mm_shader_cache.bin ./shadercache/
- name: Build N64Recomp & RSPRecomp
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout ${{ inputs.N64RECOMP_COMMIT }}
git submodule update --init --recursive
# enable ccache
set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH"
$cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config Release --target N64Recomp -j 8
cmake --build cmake-build --config Release --target RSPRecomp -j 8
cmake --build cmake-build --config Release --target N64Recomp -j $cpuCores
cmake --build cmake-build --config Release --target RSPRecomp -j $cpuCores
# Copy N64Recomp & RSPRecomp to root directory
cp cmake-build/N64Recomp.exe ..
@ -153,9 +241,15 @@ jobs:
run: |-
# enable ccache
set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH"
$cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DCMAKE_CXX_FLAGS="-Xclang -fcxx-exceptions"
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j 8
# remove LLVM from PATH so it doesn't overshadow the one provided by VS
$env:PATH = ($env:PATH -split ';' | Where-Object { $_ -ne 'C:\Program Files\LLVM\bin' }) -join ';'
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DCMAKE_CXX_FLAGS="-Xclang -fexceptions -Xclang -fcxx-exceptions"
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $cpuCores
env:
SDL2_VERSION: ${{ inputs.SDL2_VERSION }}
- name: Prepare Archive
run: |
Move-Item -Path "cmake-build/Zelda64Recompiled.exe" -Destination "Zelda64Recompiled.exe"
@ -173,3 +267,4 @@ jobs:
dxil.dll
SDL2.dll
assets/
gamecontrollerdb.txt

19
.github/workflows/validate_external.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: validate-external
on:
pull_request_target:
types: [opened, synchronize]
jobs:
authorize:
if: github.repository != github.event.pull_request.head.repo.full_name
environment:
${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo ✓
build:
needs: authorize
uses: ./.github/workflows/validate.yml
secrets:
ZRE_REPO_WITH_PAT: ${{ secrets.ZRE_REPO_WITH_PAT }}

12
.github/workflows/validate_internal.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: validate-internal
on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize]
jobs:
build:
if: github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name
uses: ./.github/workflows/validate.yml
secrets: inherit

5
.gitignore vendored
View File

@ -1,6 +1,7 @@
# VSCode file settings
.vscode/settings.json
.vscode/c_cpp_properties.json
.vscode/launch.json
# Input elf and rom files
*.elf
@ -56,3 +57,7 @@ node_modules/
# Recompiler Linux binary
N64Recomp
.DS_Store
# Controller mappings file
gamecontrollerdb.txt

3
.gitmodules vendored
View File

@ -16,3 +16,6 @@
[submodule "lib/sse2neon"]
path = lib/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
[submodule "lib/N64ModernRuntime"]
path = lib/N64ModernRuntime
url = https://github.com/N64Recomp/N64ModernRuntime.git

View File

@ -13,10 +13,6 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
endif()
if(UNIX AND NOT APPLE)
set(LINUX TRUE)
endif()
set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
@ -35,6 +31,8 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/lib/lunasvg)
SET(ENABLE_SVG_PLUGIN ON CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
target_include_directories(rt64 PRIVATE ${CMAKE_BINARY_DIR}/rt64/src)
# RecompiledFuncs - Library containing the primary recompiler output
@ -43,11 +41,14 @@ add_library(RecompiledFuncs STATIC)
target_compile_options(RecompiledFuncs PRIVATE
# -Wno-unused-but-set-variable
-fno-strict-aliasing
-Wno-unused-variable
-Wno-implicit-function-declaration
)
target_include_directories(RecompiledFuncs PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
)
file(GLOB FUNC_C_SOURCES ${CMAKE_SOURCE_DIR}/RecompiledFuncs/*.c)
@ -61,10 +62,14 @@ add_library(PatchesLib STATIC)
target_compile_options(PatchesLib PRIVATE
# -Wno-unused-but-set-variable
-fno-strict-aliasing
-Wno-unused-variable
-Wno-implicit-function-declaration
)
target_include_directories(PatchesLib PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
)
target_sources(PatchesLib PRIVATE
@ -75,8 +80,20 @@ target_sources(PatchesLib PRIVATE
set_source_files_properties(${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c PROPERTIES COMPILE_FLAGS -fno-strict-aliasing)
# Build patches elf
if(NOT DEFINED PATCHES_C_COMPILER)
set(PATCHES_C_COMPILER clang)
endif()
if(NOT DEFINED PATCHES_LD)
set(PATCHES_LD ld.lld)
endif()
if(NOT DEFINED PATCHES_OBJCOPY)
set(PATCHES_OBJCOPY llvm-objcopy)
endif()
add_custom_target(PatchesBin
COMMAND make
COMMAND ${CMAKE_COMMAND} -E env CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD} OBJCOPY=${PATCHES_OBJCOPY} make
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/patches
BYPRODUCTS ${CMAKE_SOURCE_DIR}/patches/patches.bin
)
@ -92,14 +109,25 @@ add_custom_command(OUTPUT
${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c
${CMAKE_SOURCE_DIR}/RecompiledPatches/recomp_overlays.inl
${CMAKE_SOURCE_DIR}/RecompiledPatches/funcs.h
${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
COMMAND ./N64Recomp patches.toml && ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
# TODO: Look into why modifying patches requires two builds to take
COMMAND ./N64Recomp patches.toml
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin
)
# Download controller db file for controller support via SDL2
set(GAMECONTROLLERDB_COMMIT "b1e4090b3d4266e55feb0793efa35792e05faf66")
set(GAMECONTROLLERDB_URL "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/${GAMECONTROLLERDB_COMMIT}/gamecontrollerdb.txt")
file(DOWNLOAD ${GAMECONTROLLERDB_URL} ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt
TLS_VERIFY ON)
add_custom_target(DownloadGameControllerDB
DEPENDS ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt)
# Main executable
add_executable(Zelda64Recompiled)
add_dependencies(Zelda64Recompiled DownloadGameControllerDB)
# Generate mm_shader_cache.c from the MM shader cache if it exists
if (EXISTS ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin)
@ -112,39 +140,11 @@ if (EXISTS ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin)
endif()
set (SOURCES
${CMAKE_SOURCE_DIR}/ultramodern/audio.cpp
${CMAKE_SOURCE_DIR}/ultramodern/events.cpp
${CMAKE_SOURCE_DIR}/ultramodern/mesgqueue.cpp
${CMAKE_SOURCE_DIR}/ultramodern/misc_ultra.cpp
${CMAKE_SOURCE_DIR}/ultramodern/port_main.c
${CMAKE_SOURCE_DIR}/ultramodern/scheduling.cpp
${CMAKE_SOURCE_DIR}/ultramodern/threadqueue.cpp
${CMAKE_SOURCE_DIR}/ultramodern/task_win32.cpp
${CMAKE_SOURCE_DIR}/ultramodern/threads.cpp
${CMAKE_SOURCE_DIR}/ultramodern/timer.cpp
${CMAKE_SOURCE_DIR}/ultramodern/ultrainit.cpp
${CMAKE_SOURCE_DIR}/ultramodern/rt64_layer.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ai.cpp
${CMAKE_SOURCE_DIR}/src/recomp/cont.cpp
${CMAKE_SOURCE_DIR}/src/recomp/dp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/eep.cpp
${CMAKE_SOURCE_DIR}/src/recomp/euc-jp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/flash.cpp
${CMAKE_SOURCE_DIR}/src/recomp/math_routines.cpp
${CMAKE_SOURCE_DIR}/src/recomp/overlays.cpp
${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
${CMAKE_SOURCE_DIR}/src/recomp/pak.cpp
${CMAKE_SOURCE_DIR}/src/recomp/pi.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ultra_stubs.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ultra_translation.cpp
${CMAKE_SOURCE_DIR}/src/recomp/print.cpp
${CMAKE_SOURCE_DIR}/src/recomp/recomp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/sp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/vi.cpp
${CMAKE_SOURCE_DIR}/src/main/main.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
${CMAKE_SOURCE_DIR}/src/main/rt64_render_context.cpp
${CMAKE_SOURCE_DIR}/src/game/input.cpp
${CMAKE_SOURCE_DIR}/src/game/controls.cpp
${CMAKE_SOURCE_DIR}/src/game/config.cpp
@ -203,11 +203,17 @@ endif()
if (WIN32)
include(FetchContent)
if (DEFINED ENV{SDL2_VERSION})
set(SDL2_VERSION $ENV{SDL2_VERSION})
else()
set(SDL2_VERSION "2.30.3")
endif()
# Fetch SDL2 on windows
FetchContent_Declare(
sdl2
URL https://github.com/libsdl-org/SDL/releases/download/release-2.28.5/SDL2-devel-2.28.5-VC.zip
URL_HASH MD5=d8173db078e54040c666f411c5a6afff
URL https://github.com/libsdl-org/SDL/releases/download/release-${SDL2_VERSION}/SDL2-devel-${SDL2_VERSION}-VC.zip
)
FetchContent_MakeAvailable(sdl2)
target_include_directories(Zelda64Recompiled PRIVATE
@ -224,7 +230,7 @@ if (WIN32)
"${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxil.dll"
"${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxcompiler.dll"
$<TARGET_FILE_DIR:Zelda64Recompiled>)
set_target_properties(
Zelda64Recompiled
PROPERTIES
@ -237,10 +243,10 @@ if (WIN32)
target_sources(Zelda64Recompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
endif()
if (LINUX)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(SDL2 REQUIRED)
find_package(X11 REQUIRED)
# Generate icon_bytes.c from the app icon PNG.
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
@ -273,7 +279,7 @@ if (LINUX)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
target_link_libraries(Zelda64Recompiled PRIVATE "-latomic -static-libstdc++" ${CMAKE_DL_LIBS} Threads::Threads)
endif()
@ -281,6 +287,8 @@ target_link_libraries(Zelda64Recompiled PRIVATE
PatchesLib
RecompiledFuncs
SDL2
librecomp
ultramodern
rt64
RmlCore
RmlDebugger
@ -303,15 +311,12 @@ if (${WIN32})
set (DXC "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc.exe")
add_compile_definitions(NOMINMAX)
else()
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
if (APPLE)
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
else()
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
endif()
if (APPLE)
# Apple's binary is universal, so it'll work on both x86_64 and arm64
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
else()
if (APPLE)
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
else()
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-linux")
endif()
@ -319,7 +324,7 @@ else()
endif()
build_vertex_shader(Zelda64Recompiled "shaders/InterfaceVS.hlsl" "shaders/InterfaceVS.hlsl")
build_pixel_shader (Zelda64Recompiled "shaders/InterfacePS.hlsl" "shaders/InterfacePS.hlsl")
build_pixel_shader(Zelda64Recompiled "shaders/InterfacePS.hlsl" "shaders/InterfacePS.hlsl")
target_sources(Zelda64Recompiled PRIVATE ${SOURCES})

View File

@ -5,6 +5,20 @@ Zelda 64: Recompiled is a project that uses [N64: Recompiled](https://github.com
### **This repository and its releases do not contain game assets. The original game is required to build or run this project.**
<div align="left" valign="middle">
<a href="https://runblaze.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.runblaze.dev/logo_dark.png">
<img align="right" src="https://www.runblaze.dev/logo_light.png" height="102px"/>
</picture>
</a>
<br style="display: none;"/>
_Thank you [Blaze](https://runblaze.dev) for supporting this project by providing Linux ARM64 and Apple Silicon macOS Github Action Runners!_
</div>
## Table of Contents
* [System Requirements](#system-requirements)
* [Features](#features)
@ -13,7 +27,9 @@ Zelda 64: Recompiled is a project that uses [N64: Recompiled](https://github.com
* [Easy-to-Use Menus](#easy-to-use-menus)
* [High Framerate Support](#high-framerate-support)
* [Widescreen and Ultrawide Support](#widescreen-and-ultrawide-support)
* [Dual Analog Camera](#dual-analog-camera)
* [Gyro Aim](#gyro-aim)
* [Additional Control Options](#additional-control-options)
* [Autosaving](#autosaving)
* [Low Input Lag](#low-input-lag)
* [Instant Load Times](#instant-load-times)
@ -55,11 +71,17 @@ Any aspect ratio is supported, with most effects modded to work correctly in wid
**Note**: Some animation quirks can be seen at the edges of the screen in certain cutscenes when using very wide aspect ratios.
#### Dual Analog Camera
Play with a dual analog control layout like later entries in the series! When this option is enabled, the right stick will control the camera. You can still have the C-Buttons mapped to the right stick if you so wish, so long as you also map them to other buttons on the controller. The right stick C-button inputs will be "silenced", except when you take out the ocarina, so you can still play the ocarina with the right stick.
#### Gyro Aim
When playing with a supported controller, first-person items such as the bow can be aimed with your controller's gyro sensor. This includes (but is not limited to) controllers such as the Dualshock 4, Dualsense, Switch Pro, and most third party Switch controllers (such as the 8BitDo Pro 2 in Switch mode).
**Note**: Gamepad mappers such as BetterJoy or DS4Windows may intercept gyro data and prevent the game from receiving it. Most controllers are natively supported, so turning gamepad mappers off is recommended if you want to use gyro.
#### Additional Control Options
Customize your experience by setting your stick deadzone to your liking, as well as adjusting the X and Y axis inversion for both aiming and the optional dual analog camera.
#### Autosaving
Never worry about losing progress if your power goes out thanks to autosaving! The autosave system is designed to respect Majora's Mask's original save system and maintain the intention of owl saves by triggering automatically and replacing the previous autosave or owl save. However, if you'd still rather play with the untouched save system, simply turn off autosaving in the ingame menu.
@ -75,8 +97,6 @@ A Linux binary is available for playing on most up-to-date distros, including on
To play on Steam Deck, extract the Linux build onto your deck. Then, in desktop mode, right click the Zelda64Recompiled executable file and select "Add to Steam". From there, you can return to Gaming mode and configure the controls as needed. See the [Steam Deck gyro aim FAQ section](#how-do-i-set-up-gyro-aiming-on-steam-deck) for more detailed instructions.
## Planned Features
* Dual analog control scheme (with analog camera)
* Configurable deadzone and analog stick sensitivity
* Ocarina of Time support
* Mod support and Randomizer
* Texture Packs

View File

@ -331,13 +331,13 @@
<b>Note: This option does not allow mouse buttons to activate items. Mouse aiming is intended to be used with inputs that are mapped to mouse movement, such as gyro on Steam Deck.</b>
</p>
<p data-if="cur_config_index == 4">
Applies a deadzone to joystick inputs.
</p>
<p data-if="cur_config_index == 5">
Allows the game to read controller input when out of focus.
<br/>
<b>This setting does not affect keyboard input.</b>
</p>
<p data-if="cur_config_index == 5">
Applies a deadzone to joystick inputs.
</p>
<p data-if="cur_config_index == 6">
Turns on autosaving and prevents owl saves from being deleted on load. Autosaves act as owl saves and take up the same slot as they do.
<br/>

View File

@ -1,4 +0,0 @@
#ifdef __clang__
#pragma clang diagnostic ignored "-Wunused-variable"
#pragma clang diagnostic ignored "-Wimplicit-function-declaration"
#endif

9
include/ovl_patches.hpp Normal file
View File

@ -0,0 +1,9 @@
#ifndef __OVL_PATCHES_HPP__
#define __OVL_PATCHES_HPP__
namespace zelda64 {
void register_overlays();
void register_patches();
}
#endif

View File

@ -1,327 +0,0 @@
#ifndef __RECOMP_H__
#define __RECOMP_H__
#include <stdint.h>
#include <math.h>
#include <assert.h>
#include <setjmp.h>
#include <malloc.h>
#if 0 // treat GPRs as 32-bit, should be better codegen
typedef uint32_t gpr;
#define SIGNED(val) \
((int32_t)(val))
#else
typedef uint64_t gpr;
#define SIGNED(val) \
((int64_t)(val))
#endif
#define ADD32(a, b) \
((gpr)(int32_t)((a) + (b)))
#define SUB32(a, b) \
((gpr)(int32_t)((a) - (b)))
#define MEM_W(offset, reg) \
(*(int32_t*)(rdram + ((((reg) + (offset))) - 0xFFFFFFFF80000000)))
//(*(int32_t*)(rdram + ((((reg) + (offset))) & 0x3FFFFFF)))
#define MEM_H(offset, reg) \
(*(int16_t*)(rdram + ((((reg) + (offset)) ^ 2) - 0xFFFFFFFF80000000)))
//(*(int16_t*)(rdram + ((((reg) + (offset)) ^ 2) & 0x3FFFFFF)))
#define MEM_B(offset, reg) \
(*(int8_t*)(rdram + ((((reg) + (offset)) ^ 3) - 0xFFFFFFFF80000000)))
//(*(int8_t*)(rdram + ((((reg) + (offset)) ^ 3) & 0x3FFFFFF)))
#define MEM_HU(offset, reg) \
(*(uint16_t*)(rdram + ((((reg) + (offset)) ^ 2) - 0xFFFFFFFF80000000)))
//(*(uint16_t*)(rdram + ((((reg) + (offset)) ^ 2) & 0x3FFFFFF)))
#define MEM_BU(offset, reg) \
(*(uint8_t*)(rdram + ((((reg) + (offset)) ^ 3) - 0xFFFFFFFF80000000)))
//(*(uint8_t*)(rdram + ((((reg) + (offset)) ^ 3) & 0x3FFFFFF)))
#define SD(val, offset, reg) { \
*(uint32_t*)(rdram + ((((reg) + (offset) + 4)) - 0xFFFFFFFF80000000)) = (uint32_t)((gpr)(val) >> 0); \
*(uint32_t*)(rdram + ((((reg) + (offset) + 0)) - 0xFFFFFFFF80000000)) = (uint32_t)((gpr)(val) >> 32); \
}
//#define SD(val, offset, reg) { \
// *(uint32_t*)(rdram + ((((reg) + (offset) + 4)) & 0x3FFFFFF)) = (uint32_t)((val) >> 32); \
// *(uint32_t*)(rdram + ((((reg) + (offset) + 0)) & 0x3FFFFFF)) = (uint32_t)((val) >> 0); \
//}
static inline uint64_t load_doubleword(uint8_t* rdram, gpr reg, gpr offset) {
uint64_t ret = 0;
uint64_t lo = (uint64_t)(uint32_t)MEM_W(reg, offset + 4);
uint64_t hi = (uint64_t)(uint32_t)MEM_W(reg, offset + 0);
ret = (lo << 0) | (hi << 32);
return ret;
}
#define LD(offset, reg) \
load_doubleword(rdram, offset, reg)
static inline gpr do_lwl(uint8_t* rdram, gpr initial_value, gpr offset, gpr reg) {
// Calculate the overall address
gpr address = (offset + reg);
// Load the aligned word
gpr word_address = address & ~0x3;
uint32_t loaded_value = MEM_W(0, word_address);
// Mask the existing value and shift the loaded value appropriately
gpr misalignment = address & 0x3;
gpr masked_value = initial_value & ~(0xFFFFFFFFu << (misalignment * 8));
loaded_value <<= (misalignment * 8);
// Cast to int32_t to sign extend first
return (gpr)(int32_t)(masked_value | loaded_value);
}
static inline gpr do_lwr(uint8_t* rdram, gpr initial_value, gpr offset, gpr reg) {
// Calculate the overall address
gpr address = (offset + reg);
// Load the aligned word
gpr word_address = address & ~0x3;
uint32_t loaded_value = MEM_W(0, word_address);
// Mask the existing value and shift the loaded value appropriately
gpr misalignment = address & 0x3;
gpr masked_value = initial_value & ~(0xFFFFFFFFu >> (24 - misalignment * 8));
loaded_value >>= (24 - misalignment * 8);
// Cast to int32_t to sign extend first
return (gpr)(int32_t)(masked_value | loaded_value);
}
static inline void do_swl(uint8_t* rdram, gpr offset, gpr reg, gpr val) {
// Calculate the overall address
gpr address = (offset + reg);
// Get the initial value of the aligned word
gpr word_address = address & ~0x3;
uint32_t initial_value = MEM_W(0, word_address);
// Mask the initial value and shift the input value appropriately
gpr misalignment = address & 0x3;
uint32_t masked_initial_value = initial_value & ~(0xFFFFFFFFu >> (misalignment * 8));
uint32_t shifted_input_value = ((uint32_t)val) >> (misalignment * 8);
MEM_W(0, word_address) = masked_initial_value | shifted_input_value;
}
static inline void do_swr(uint8_t* rdram, gpr offset, gpr reg, gpr val) {
// Calculate the overall address
gpr address = (offset + reg);
// Get the initial value of the aligned word
gpr word_address = address & ~0x3;
uint32_t initial_value = MEM_W(0, word_address);
// Mask the initial value and shift the input value appropriately
gpr misalignment = address & 0x3;
uint32_t masked_initial_value = initial_value & ~(0xFFFFFFFFu << (24 - misalignment * 8));
uint32_t shifted_input_value = ((uint32_t)val) << (24 - misalignment * 8);
MEM_W(0, word_address) = masked_initial_value | shifted_input_value;
}
#define S32(val) \
((int32_t)(val))
#define U32(val) \
((uint32_t)(val))
#define S64(val) \
((int64_t)(val))
#define U64(val) \
((uint64_t)(val))
#define MUL_S(val1, val2) \
((val1) * (val2))
#define MUL_D(val1, val2) \
((val1) * (val2))
#define DIV_S(val1, val2) \
((val1) / (val2))
#define DIV_D(val1, val2) \
((val1) / (val2))
#define CVT_S_W(val) \
((float)((int32_t)(val)))
#define CVT_D_W(val) \
((double)((int32_t)(val)))
#define CVT_D_S(val) \
((double)(val))
#define CVT_S_D(val) \
((float)(val))
#define TRUNC_W_S(val) \
((int32_t)(val))
#define TRUNC_W_D(val) \
((int32_t)(val))
#define TRUNC_L_S(val) \
((int64_t)(val))
#define TRUNC_L_D(val) \
((int64_t)(val))
#define DEFAULT_ROUNDING_MODE 0
static inline int32_t do_cvt_w_s(float val, unsigned int rounding_mode) {
switch (rounding_mode) {
case 0: // round to nearest value
return (int32_t)lroundf(val);
case 1: // round to zero (truncate)
return (int32_t)val;
case 2: // round to positive infinity (ceil)
return (int32_t)ceilf(val);
case 3: // round to negative infinity (floor)
return (int32_t)floorf(val);
}
assert(0);
return 0;
}
#define CVT_W_S(val) \
do_cvt_w_s(val, rounding_mode)
static inline int32_t do_cvt_w_d(double val, unsigned int rounding_mode) {
switch (rounding_mode) {
case 0: // round to nearest value
return (int32_t)lround(val);
case 1: // round to zero (truncate)
return (int32_t)val;
case 2: // round to positive infinity (ceil)
return (int32_t)ceil(val);
case 3: // round to negative infinity (floor)
return (int32_t)floor(val);
}
assert(0);
return 0;
}
#define CVT_W_D(val) \
do_cvt_w_d(val, rounding_mode)
#define NAN_CHECK(val) \
assert(val == val)
//#define NAN_CHECK(val)
typedef union {
double d;
struct {
float fl;
float fh;
};
struct {
uint32_t u32l;
uint32_t u32h;
};
uint64_t u64;
} fpr;
typedef struct {
gpr r0, r1, r2, r3, r4, r5, r6, r7,
r8, r9, r10, r11, r12, r13, r14, r15,
r16, r17, r18, r19, r20, r21, r22, r23,
r24, r25, r26, r27, r28, r29, r30, r31;
fpr f0, f1, f2, f3, f4, f5, f6, f7,
f8, f9, f10, f11, f12, f13, f14, f15,
f16, f17, f18, f19, f20, f21, f22, f23,
f24, f25, f26, f27, f28, f29, f30, f31;
uint64_t hi, lo;
uint32_t* f_odd;
uint32_t status_reg;
uint8_t mips3_float_mode;
} recomp_context;
// Checks if the target is an even float register or that mips3 float mode is enabled
#define CHECK_FR(ctx, idx) \
assert(((idx) & 1) == 0 || (ctx)->mips3_float_mode)
#ifdef __cplusplus
extern "C" {
#endif
void cop0_status_write(recomp_context* ctx, gpr value);
gpr cop0_status_read(recomp_context* ctx);
void switch_error(const char* func, uint32_t vram, uint32_t jtbl);
void do_break(uint32_t vram);
typedef void (recomp_func_t)(uint8_t* rdram, recomp_context* ctx);
recomp_func_t* get_function(int32_t vram);
#define LOOKUP_FUNC(val) \
get_function((int32_t)(val))
extern int32_t section_addresses[];
#define LO16(x) \
((x) & 0xFFFF)
#define HI16(x) \
(((x) >> 16) + (((x) >> 15) & 1))
#define RELOC_HI16(section_index, offset) \
HI16(section_addresses[section_index] + (offset))
#define RELOC_LO16(section_index, offset) \
LO16(section_addresses[section_index] + (offset))
// For Banjo-Tooie
void recomp_syscall_handler(uint8_t* rdram, recomp_context* ctx, int32_t instruction_vram);
// For the Mario Party games (not working)
//// This has to be in this file so it can be inlined
//struct jmp_buf_storage {
// jmp_buf buffer;
//};
//
//struct RecompJmpBuf {
// int32_t owner;
// struct jmp_buf_storage* storage;
// uint64_t magic;
//};
//
//// Randomly generated constant
//#define SETJMP_MAGIC 0xe17afdfa939a437bu
//
//int32_t osGetThreadEx(void);
//
//#define setjmp_recomp(rdram, ctx) { \
// struct RecompJmpBuf* buf = (struct RecompJmpBuf*)(&rdram[(uint64_t)ctx->r4 - 0xFFFFFFFF80000000]); \
// \
// /* Check if this jump buffer was previously set up */ \
// if (buf->magic == SETJMP_MAGIC) { \
// /* If so, free the old jmp_buf */ \
// free(buf->storage); \
// } \
// \
// buf->magic = SETJMP_MAGIC; \
// buf->owner = osGetThreadEx(); \
// buf->storage = (struct jmp_buf_storage*)calloc(1, sizeof(struct jmp_buf_storage)); \
// ctx->r2 = setjmp(buf->storage->buffer); \
//}
void pause_self(uint8_t *rdram);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,54 +0,0 @@
#ifndef __RECOMP_CONFIG_H__
#define __RECOMP_CONFIG_H__
#include <filesystem>
#include <string_view>
#include "../ultramodern/config.hpp"
namespace recomp {
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr std::u8string_view mm_game_id = u8"mm.n64.us.1.0";
constexpr std::string_view program_name = "Zelda 64: Recompiled";
void load_config();
void save_config();
void reset_input_bindings();
void reset_cont_input_bindings();
void reset_kb_input_bindings();
std::filesystem::path get_app_folder_path();
bool get_debug_mode_enabled();
void set_debug_mode_enabled(bool enabled);
enum class AutosaveMode {
On,
Off,
OptionCount
};
enum class AnalogCamMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::AutosaveMode, {
{recomp::AutosaveMode::On, "On"},
{recomp::AutosaveMode::Off, "Off"}
});
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::AnalogCamMode, {
{recomp::AnalogCamMode::On, "On"},
{recomp::AnalogCamMode::Off, "Off"}
});
AutosaveMode get_autosave_mode();
void set_autosave_mode(AutosaveMode mode);
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
};
#endif

14
include/recomp_files.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef __RECOMP_FILES_H__
#define __RECOMP_FILES_H__
#include <filesystem>
#include <fstream>
namespace recomp {
std::ifstream open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in);
std::ifstream open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in);
std::ofstream open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::out);
bool finalize_output_file_with_backup(const std::filesystem::path& filepath);
};
#endif

View File

@ -1,40 +0,0 @@
#ifndef __RECOMP_GAME__
#define __RECOMP_GAME__
#include <vector>
#include <filesystem>
#include "recomp.h"
#include "../ultramodern/ultramodern.hpp"
#include "rt64_layer.h"
namespace recomp {
enum class Game {
OoT,
MM,
None,
Quit
};
enum class RomValidationError {
Good,
FailedToOpen,
NotARom,
IncorrectRom,
NotYet,
IncorrectVersion,
OtherError
};
void check_all_stored_roms();
bool load_stored_rom(Game game);
RomValidationError select_rom(const std::filesystem::path& rom_path, Game game);
bool is_rom_valid(Game game);
bool is_rom_loaded();
void set_rom_contents(std::vector<uint8_t>&& new_rom);
void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes);
void do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr);
void start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks);
void start_game(Game game);
void message_box(const char* message);
}
#endif

View File

@ -1,50 +0,0 @@
#ifndef __RECOMP_HELPERS__
#define __RECOMP_HELPERS__
#include "recomp.h"
#include "../ultramodern/ultra64.h"
template<int index, typename T>
T _arg(uint8_t* rdram, recomp_context* ctx) {
static_assert(index < 4, "Only args 0 through 3 supported");
gpr raw_arg = (&ctx->r4)[index];
if constexpr (std::is_same_v<T, float>) {
if constexpr (index < 2) {
static_assert(index != 1, "Floats in arg 1 not supported");
return ctx->f12.fl;
}
else {
// static_assert in else workaround
[] <bool flag = false>() {
static_assert(flag, "Floats in a2/a3 not supported");
}();
}
}
else if constexpr (std::is_pointer_v<T>) {
static_assert (!std::is_pointer_v<std::remove_pointer_t<T>>, "Double pointers not supported");
return TO_PTR(std::remove_pointer_t<T>, raw_arg);
}
else if constexpr (std::is_integral_v<T>) {
static_assert(sizeof(T) <= 4, "64-bit args not supported");
return static_cast<T>(raw_arg);
}
else {
// static_assert in else workaround
[] <bool flag = false>() {
static_assert(flag, "Unsupported type");
}();
}
}
template <typename T>
void _return(recomp_context* ctx, T val) {
static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently");
if (std::is_same_v<T, float>) {
ctx->f0.fl = val;
}
else if (std::is_integral_v<T> && sizeof(T) <= 4) {
ctx->r2 = int32_t(val);
}
}
#endif

View File

@ -9,11 +9,14 @@
#include <string>
#include <string_view>
#include "ultramodern/input.hpp"
#include "json/json.hpp"
namespace recomp {
// x-macros to build input enums and arrays.
// First parameter is the enum name, second parameter is the bit field for the input (or 0 if there is no associated one), third is the readable name.
// TODO refactor this to allow projects to rename these, or get rid of the readable name and leave that up to individual projects to map.
#define DEFINE_N64_BUTTON_INPUTS() \
DEFINE_INPUT(A, 0x8000, "Action") \
DEFINE_INPUT(B, 0x4000, "Attack/Cancel") \
@ -149,10 +152,12 @@ namespace recomp {
InputField& get_input_binding(GameInput input, size_t binding_index, InputDevice device);
void set_input_binding(GameInput input, size_t binding_index, InputDevice device, InputField value);
void get_n64_input(uint16_t* buttons_out, float* x_out, float* y_out);
void set_rumble(bool);
bool get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out);
void set_rumble(int controller_num, bool);
void update_rumble();
void handle_events();
ultramodern::input::connected_device_info_t get_connected_device_info(int controller_num);
// Rumble strength ranges from 0 to 100.
int get_rumble_strength();
@ -168,20 +173,6 @@ namespace recomp {
void apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out);
void set_right_analog_suppressed(bool suppressed);
enum class TargetingMode {
Switch,
Hold,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::TargetingMode, {
{recomp::TargetingMode::Switch, "Switch"},
{recomp::TargetingMode::Hold, "Hold"}
});
TargetingMode get_targeting_mode();
void set_targeting_mode(TargetingMode mode);
enum class BackgroundInputMode {
On,
Off,
@ -196,35 +187,8 @@ namespace recomp {
BackgroundInputMode get_background_input_mode();
void set_background_input_mode(BackgroundInputMode mode);
enum class CameraInvertMode {
InvertNone,
InvertX,
InvertY,
InvertBoth,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::CameraInvertMode, {
{recomp::CameraInvertMode::InvertNone, "InvertNone"},
{recomp::CameraInvertMode::InvertX, "InvertX"},
{recomp::CameraInvertMode::InvertY, "InvertY"},
{recomp::CameraInvertMode::InvertBoth, "InvertBoth"}
});
CameraInvertMode get_camera_invert_mode();
void set_camera_invert_mode(CameraInvertMode mode);
CameraInvertMode get_analog_camera_invert_mode();
void set_analog_camera_invert_mode(CameraInvertMode mode);
bool game_input_disabled();
bool all_input_disabled();
// TODO move these
void quicksave_save();
void quicksave_load();
void open_quit_game_prompt();
}
#endif

View File

@ -1,10 +0,0 @@
#ifndef __RECOMP_OVERLAYS_H__
#define __RECOMP_OVERLAYS_H__
#include <cstdint>
extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size);
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size);
void init_overlays();
#endif

View File

@ -14,7 +14,7 @@ namespace Rml {
class Event;
}
namespace recomp {
namespace recompui {
class UiEventListenerInstancer;
class MenuController {
@ -118,6 +118,10 @@ namespace recomp {
bool get_cont_active(void);
void set_cont_active(bool active);
void activate_mouse();
void message_box(const char* msg);
void set_render_hooks();
}
#endif

View File

@ -1,94 +0,0 @@
#ifndef __RSP_H__
#define __RSP_H__
#include "rsp_vu.h"
#include "recomp.h"
#include <cstdio>
enum class RspExitReason {
Invalid,
Broke,
ImemOverrun,
UnhandledJumpTarget,
Unsupported
};
extern uint8_t dmem[];
extern uint16_t rspReciprocals[512];
extern uint16_t rspInverseSquareRoots[512];
#define RSP_MEM_B(offset, addr) \
(*reinterpret_cast<int8_t*>(dmem + (0xFFF & (((offset) + (addr)) ^ 3))))
#define RSP_MEM_BU(offset, addr) \
(*reinterpret_cast<uint8_t*>(dmem + (0xFFF & (((offset) + (addr)) ^ 3))))
static inline uint32_t RSP_MEM_W_LOAD(uint32_t offset, uint32_t addr) {
uint32_t out;
for (int i = 0; i < 4; i++) {
reinterpret_cast<uint8_t*>(&out)[i ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline void RSP_MEM_W_STORE(uint32_t offset, uint32_t addr, uint32_t val) {
for (int i = 0; i < 4; i++) {
RSP_MEM_BU(offset + i, addr) = reinterpret_cast<uint8_t*>(&val)[i ^ 3];
}
}
static inline uint32_t RSP_MEM_HU_LOAD(uint32_t offset, uint32_t addr) {
uint16_t out;
for (int i = 0; i < 2; i++) {
reinterpret_cast<uint8_t*>(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline uint32_t RSP_MEM_H_LOAD(uint32_t offset, uint32_t addr) {
int16_t out;
for (int i = 0; i < 2; i++) {
reinterpret_cast<uint8_t*>(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline void RSP_MEM_H_STORE(uint32_t offset, uint32_t addr, uint32_t val) {
for (int i = 0; i < 2; i++) {
RSP_MEM_BU(offset + i, addr) = reinterpret_cast<uint8_t*>(&val)[(i + 2) ^ 3];
}
}
#define RSP_ADD32(a, b) \
((int32_t)((a) + (b)))
#define RSP_SUB32(a, b) \
((int32_t)((a) - (b)))
#define RSP_SIGNED(val) \
((int32_t)(val))
#define SET_DMA_DMEM(dmem_addr) dma_dmem_address = (dmem_addr)
#define SET_DMA_DRAM(dram_addr) dma_dram_address = (dram_addr)
#define DO_DMA_READ(rd_len) dma_rdram_to_dmem(rdram, dma_dmem_address, dma_dram_address, (rd_len))
#define DO_DMA_WRITE(wr_len) dma_dmem_to_rdram(rdram, dma_dmem_address, dma_dram_address, (wr_len))
static inline void dma_rdram_to_dmem(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t rd_len) {
rd_len += 1; // Read length is inclusive
dram_addr &= 0xFFFFF8;
assert(dmem_addr + rd_len <= 0x1000);
for (uint32_t i = 0; i < rd_len; i++) {
RSP_MEM_B(i, dmem_addr) = MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000));
}
}
static inline void dma_dmem_to_rdram(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t wr_len) {
wr_len += 1; // Write length is inclusive
dram_addr &= 0xFFFFF8;
assert(dmem_addr + wr_len <= 0x1000);
for (uint32_t i = 0; i < wr_len; i++) {
MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000)) = RSP_MEM_B(i, dmem_addr);
}
}
#endif

View File

@ -1,203 +0,0 @@
// This file is modified from the Ares N64 emulator core. Ares can
// be found at https://github.com/ares-emulator/ares. The original license
// for this portion of Ares is as follows:
// ----------------------------------------------------------------------
// ares
//
// Copyright(c) 2004 - 2021 ares team, Near et al
//
// Permission to use, copy, modify, and /or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright noticeand this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS.IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// ----------------------------------------------------------------------
#include <cstdint>
#if defined(__x86_64__) || defined(_M_X64)
#define ARCHITECTURE_SUPPORTS_SSE4_1 1
#include <nmmintrin.h>
using v128 = __m128i;
#elif defined(__aarch64__) || defined(_M_ARM64)
#define ARCHITECTURE_SUPPORTS_SSE4_1 1
#include "sse2neon.h"
using v128 = __m128i;
#endif
namespace Accuracy {
namespace RSP {
#if ARCHITECTURE_SUPPORTS_SSE4_1
constexpr bool SISD = false;
constexpr bool SIMD = true;
#else
constexpr bool SISD = true;
constexpr bool SIMD = false;
#endif
}
}
using u8 = uint8_t;
using s8 = int8_t;
using u16 = uint16_t;
using s16 = int16_t;
using u32 = uint32_t;
using s32 = int32_t;
using u64 = uint64_t;
using s64 = int64_t;
using uint128_t = uint64_t[2];
template<u32 bits> inline auto sclamp(s64 x) -> s64 {
enum : s64 { b = 1ull << (bits - 1), m = b - 1 };
return (x > m) ? m : (x < -b) ? -b : x;
}
template<u32 bits> inline auto sclip(s64 x) -> s64 {
enum : u64 { b = 1ull << (bits - 1), m = b * 2 - 1 };
return ((x & m) ^ b) - b;
}
struct RSP {
using r32 = uint32_t;
using cr32 = const r32;
union r128 {
struct { uint64_t u128[2]; };
#if ARCHITECTURE_SUPPORTS_SSE4_1
struct { __m128i v128; };
operator __m128i() const { return v128; }
auto operator=(__m128i value) { v128 = value; }
#endif
auto byte(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; }
auto byte(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; }
auto element(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; }
auto element(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; }
auto u8(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; }
auto u8(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; }
auto s16(u32 index) -> int16_t& { return ((int16_t*)&u128)[7 - index]; }
auto s16(u32 index) const -> int16_t { return ((int16_t*)&u128)[7 - index]; }
auto u16(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; }
auto u16(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; }
//VCx registers
auto get(u32 index) const -> bool { return u16(index) != 0; }
auto set(u32 index, bool value) -> bool { return u16(index) = 0 - value, value; }
//vu-registers.cpp
inline auto operator()(u32 index) const -> r128;
};
using cr128 = const r128;
struct VU {
r128 r[32];
r128 acch, accm, accl;
r128 vcoh, vcol; //16-bit little endian
r128 vcch, vccl; //16-bit little endian
r128 vce; // 8-bit little endian
s16 divin;
s16 divout;
bool divdp;
} vpu;
static constexpr r128 zero{0};
static constexpr r128 invert{(uint64_t)-1, (uint64_t)-1};
inline auto accumulatorGet(u32 index) const -> u64;
inline auto accumulatorSet(u32 index, u64 value) -> void;
inline auto accumulatorSaturate(u32 index, bool slice, u16 negative, u16 positive) const -> u16;
inline auto CFC2(r32& rt, u8 rd) -> void;
inline auto CTC2(cr32& rt, u8 rd) -> void;
template<u8 e> inline auto LBV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LDV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LFV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LHV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LLV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LPV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LQV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LRV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LSV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LTV(u8 vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LUV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LWV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto MFC2(r32& rt, cr128& vs) -> void;
template<u8 e> inline auto MTC2(cr32& rt, r128& vs) -> void;
template<u8 e> inline auto SBV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SDV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SFV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SHV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SLV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SPV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SQV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SRV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SSV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto STV(u8 vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SUV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SWV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto VABS(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VADD(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VADDC(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VAND(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VEQ(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VGE(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VLT(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool U, u8 e>
inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<0, e>(vd, vs, vt); }
template<u8 e> inline auto VMACU(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<1, e>(vd, vs, vt); }
inline auto VMACQ(r128& vd) -> void;
template<u8 e> inline auto VMADH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADM(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADN(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMOV(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VMRG(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDM(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDN(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool U, u8 e>
inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<0, e>(rd, vs, vt); }
template<u8 e> inline auto VMULU(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<1, e>(rd, vs, vt); }
template<u8 e> inline auto VMULQ(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNAND(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNE(r128& vd, cr128& vs, cr128& vt) -> void;
inline auto VNOP() -> void;
template<u8 e> inline auto VNOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNXOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool L, u8 e>
inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void { VRCP<0, e>(vd, de, vt); }
template<u8 e> inline auto VRCPL(r128& vd, u8 de, cr128& vt) -> void { VRCP<1, e>(vd, de, vt); }
template<u8 e> inline auto VRCPH(r128& vd, u8 de, cr128& vt) -> void;
template<bool D, u8 e>
inline auto VRND(r128& vd, u8 vs, cr128& vt) -> void;
template<u8 e> inline auto VRNDN(r128& vd, u8 vs, cr128& vt) -> void { VRND<0, e>(vd, vs, vt); }
template<u8 e> inline auto VRNDP(r128& vd, u8 vs, cr128& vt) -> void { VRND<1, e>(vd, vs, vt); }
template<bool L, u8 e>
inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void { VRSQ<0, e>(vd, de, vt); }
template<u8 e> inline auto VRSQL(r128& vd, u8 de, cr128& vt) -> void { VRSQ<1, e>(vd, de, vt); }
template<u8 e> inline auto VRSQH(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VSAR(r128& vd, cr128& vs) -> void;
template<u8 e> inline auto VSUB(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VSUBC(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VXOR(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VZERO(r128& rd, cr128& vs, cr128& vt) -> void;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +0,0 @@
#ifndef __RT64_LAYER_H__
#define __RT64_LAYER_H__
#include "../ultramodern/ultramodern.hpp"
#include "../ultramodern/config.hpp"
namespace RT64 {
struct Application;
}
namespace ultramodern {
enum class RT64SetupResult {
Success,
DynamicLibrariesNotFound,
InvalidGraphicsAPI,
GraphicsAPINotFound,
GraphicsDeviceNotFound
};
struct WindowHandle;
struct RT64Context {
public:
~RT64Context();
RT64Context(uint8_t* rdram, WindowHandle window_handle, bool developer_mode);
bool valid() { return static_cast<bool>(app); }
RT64SetupResult get_setup_result() { return setup_result; }
void update_config(const GraphicsConfig& old_config, const GraphicsConfig& new_config);
void enable_instant_present();
void send_dl(const OSTask* task);
void update_screen(uint32_t vi_origin);
void shutdown();
void set_dummy_vi();
uint32_t get_display_framerate();
float get_resolution_scale();
void load_shader_cache(std::span<const char> cache_binary);
private:
RT64SetupResult setup_result;
std::unique_ptr<RT64::Application> app;
};
RT64::UserConfiguration::Antialiasing RT64MaxMSAA();
bool RT64SamplePositionsSupported();
bool RT64HighPrecisionFBEnabled();
}
void set_rt64_hooks();
#endif

View File

@ -1,23 +0,0 @@
#ifndef __SECTIONS_H__
#define __SECTIONS_H__
#include <stdint.h>
#include "recomp.h"
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
typedef struct {
recomp_func_t* func;
uint32_t offset;
} FuncEntry;
typedef struct {
uint32_t rom_addr;
uint32_t ram_addr;
uint32_t size;
FuncEntry *funcs;
size_t num_funcs;
size_t index;
} SectionTableEntry;
#endif

91
include/zelda_config.h Normal file
View File

@ -0,0 +1,91 @@
#ifndef __ZELDA_CONFIG_H__
#define __ZELDA_CONFIG_H__
#include <filesystem>
#include <string_view>
#include "ultramodern/config.hpp"
namespace zelda64 {
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr std::string_view program_name = "Zelda 64: Recompiled";
// TODO: Move loading configs to the runtime once we have a way to allow per-project customization.
void load_config();
void save_config();
void reset_input_bindings();
void reset_cont_input_bindings();
void reset_kb_input_bindings();
std::filesystem::path get_app_folder_path();
bool get_debug_mode_enabled();
void set_debug_mode_enabled(bool enabled);
enum class AutosaveMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::AutosaveMode, {
{zelda64::AutosaveMode::On, "On"},
{zelda64::AutosaveMode::Off, "Off"}
});
enum class TargetingMode {
Switch,
Hold,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::TargetingMode, {
{zelda64::TargetingMode::Switch, "Switch"},
{zelda64::TargetingMode::Hold, "Hold"}
});
TargetingMode get_targeting_mode();
void set_targeting_mode(TargetingMode mode);
enum class CameraInvertMode {
InvertNone,
InvertX,
InvertY,
InvertBoth,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::CameraInvertMode, {
{zelda64::CameraInvertMode::InvertNone, "InvertNone"},
{zelda64::CameraInvertMode::InvertX, "InvertX"},
{zelda64::CameraInvertMode::InvertY, "InvertY"},
{zelda64::CameraInvertMode::InvertBoth, "InvertBoth"}
});
CameraInvertMode get_camera_invert_mode();
void set_camera_invert_mode(CameraInvertMode mode);
CameraInvertMode get_analog_camera_invert_mode();
void set_analog_camera_invert_mode(CameraInvertMode mode);
enum class AnalogCamMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::AnalogCamMode, {
{zelda64::AnalogCamMode::On, "On"},
{zelda64::AnalogCamMode::Off, "Off"}
});
AutosaveMode get_autosave_mode();
void set_autosave_mode(AutosaveMode mode);
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
void open_quit_game_prompt();
};
#endif

View File

@ -1,10 +1,10 @@
#ifndef __RECOMP_DEBUG_H__
#define __RECOMP_DEBUG_H__
#ifndef __ZELDA_DEBUG_H__
#define __ZELDA_DEBUG_H__
#include <vector>
#include <string>
namespace recomp {
namespace zelda64 {
struct SceneWarps {
int index;
std::string name;

9
include/zelda_game.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef __ZELDA_GAME_H__
#define __ZELDA_GAME_H__
namespace zelda64 {
void quicksave_save();
void quicksave_load();
};
#endif

42
include/zelda_render.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef __ZELDA_RENDER_H__
#define __ZELDA_RENDER_H__
#include "common/rt64_user_configuration.h"
#include "ultramodern/renderer_context.hpp"
namespace RT64 {
struct Application;
}
namespace zelda64 {
namespace renderer {
class RT64Context : public ultramodern::renderer::RendererContext {
public:
~RT64Context() override;
RT64Context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
bool valid() override { return static_cast<bool>(app); }
bool update_config(const ultramodern::renderer::GraphicsConfig &old_config, const ultramodern::renderer::GraphicsConfig &new_config) override;
void enable_instant_present() override;
void send_dl(const OSTask *task) override;
void update_screen(uint32_t vi_origin) override;
void shutdown() override;
uint32_t get_display_framerate() const override;
float get_resolution_scale() const override;
void load_shader_cache(std::span<const char> cache_binary) override;
protected:
std::unique_ptr<RT64::Application> app;
};
std::unique_ptr<ultramodern::renderer::RendererContext> create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
RT64::UserConfiguration::Antialiasing RT64MaxMSAA();
bool RT64SamplePositionsSupported();
bool RT64HighPrecisionFBEnabled();
}
}
#endif

View File

@ -1,7 +1,7 @@
#ifndef __RECOMP_SOUND_H__
#define __RECOMP_SOUND_H__
#ifndef __ZELDA_SOUND_H__
#define __ZELDA_SOUND_H__
namespace recomp {
namespace zelda64 {
void reset_sound_settings();
void set_main_volume(int volume);
int get_main_volume();

1
lib/N64ModernRuntime Submodule

@ -0,0 +1 @@
Subproject commit 0c1811ca6f8291c6608f1d6626a73e863902ece9

@ -1 +1 @@
Subproject commit 1adcbea31a04f2403da729eb5dfed3950dd7ec52
Subproject commit 55ffc5e786b47e9139cf175c3cc3af62d56d5d31

View File

@ -1,8 +1,8 @@
TARGET = patches.elf
CC := clang
LD := ld.lld
OBJCOPY := llvm-objcopy
CC ?= clang
LD ?= ld.lld
OBJCOPY ?= llvm-objcopy
CFLAGS := -target mips -mips2 -mabi=32 -O2 -G0 -mno-abicalls -mno-odd-spreg -mno-check-zero-division \
-fomit-frame-pointer -ffast-math -fno-unsafe-math-optimizations -fno-builtin-memset \
@ -27,7 +27,7 @@ $(C_OBJS): %.o : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) $< -MMD -MF $(@:.o=.d) -c -o $@
clean:
rm -rf $(C_OBJS) $(TARGET) $(DATABIN)
rm -rf $(C_OBJS) $(TARGET) $(DATABIN) $(C_DEPS)
-include $(C_DEPS)

View File

@ -4,10 +4,15 @@
#include "play_patches.h"
#include "camera_patches.h"
#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
#include "overlays/actors/ovl_Boss_04/z_boss_04.h"
#include "overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.h"
#include "z64shrink_window.h"
#include "z64player.h"
static bool prev_analog_cam_active = false;
static bool can_use_analog_cam = false;
static bool analog_cam_active = false;
static bool analog_cam_skip_once = false;
VecGeo analog_camera_pos = { .r = 66.0f, .pitch = 0, .yaw = 0 };
@ -62,6 +67,11 @@ void update_analog_cam(Camera* c) {
analog_cam_active = true;
}
if (analog_cam_skip_once) {
analog_cam_active = false;
analog_cam_skip_once = false;
}
// Record the Z targeting state.
prev_targeting_held = targeting_held;
@ -1873,3 +1883,221 @@ void analog_cam_post_play_update(PlayState* play) {
active_cam->inputDir.y = analog_camera_pos.yaw + DEG_TO_BINANG(180);
}
}
bool get_analog_cam_active() {
return analog_cam_active;
}
void set_analog_cam_active(bool isActive) {
analog_cam_active = isActive;
}
// Calling this will avoid analog cam taking over for the following game loop.
// E.g. using left stick inputs while in a deku flower taking priority over right stick.
void skip_analog_cam_once() {
analog_cam_skip_once = true;
analog_cam_active = false;
}
extern void func_809ECD00(Boss04* this, PlayState* play);
extern s32 func_800B7298(struct PlayState* play, Actor* csActor, u8 csAction);
extern u8 D_809EE4D0;
// @recomp Patch the Wart boss fight in the Great Bay temple so that the fight starts if you look at it with the right stick analog camera,
// instead of requiring entering first person mode mode.
void func_809EC568(Boss04* this, PlayState* play) {
Player* player = GET_PLAYER(play);
f32 x;
f32 y;
f32 z;
s32 pad;
// @recomp Manual relocation, TODO remove when automated.
u8* D_809EE4D0_relocated = (u8*)actor_relocate(&this->actor, &D_809EE4D0);
u16 maxProjectedPosToStartFight;
// @recomp Change the maximun projected position to start the fight depending on whether analog camera is enabled or not.
// The default vanilla value makes it so that you have to pretty much start in the corner in order to be able to get the angle it wants.
if (analog_cam_active) {
maxProjectedPosToStartFight = 600.0f;
} else {
maxProjectedPosToStartFight = 300.0f; // vanilla value
}
this->unk_704++;
this->unk_1FE = 15;
if ((this->unk_708 != 0) && (this->unk_708 < 10)) {
this->actor.world.pos.y = (Math_SinS(this->unk_1F4 * 512) * 10.0f) + (this->actor.floorHeight + 160.0f);
Matrix_RotateYS(this->actor.yawTowardsPlayer, MTXMODE_NEW);
}
switch (this->unk_708) {
case 0:
this->unk_2C8 = 50;
this->unk_2D0 = 2000.0f;
// @recomp do not require being in c-up mode if analog cam is enabled
// also, use the new variable instead of the vanilla value to check if the player is looking at the boss.
if (((player->stateFlags1 & PLAYER_STATE1_100000) || (recomp_analog_cam_enabled())) && (this->actor.projectedPos.z > 0.0f) &&
(fabsf(this->actor.projectedPos.x) < maxProjectedPosToStartFight) && (fabsf(this->actor.projectedPos.y) < maxProjectedPosToStartFight)) {
if ((this->unk_704 >= 15) && (CutsceneManager_GetCurrentCsId() == CS_ID_NONE)) {
Actor* boss;
this->unk_708 = 10;
this->unk_704 = 0;
Cutscene_StartManual(play, &play->csCtx);
this->subCamId = Play_CreateSubCamera(play);
Play_ChangeCameraStatus(play, CAM_ID_MAIN, CAM_STATUS_WAIT);
Play_ChangeCameraStatus(play, this->subCamId, CAM_STATUS_ACTIVE);
func_800B7298(play, &this->actor, PLAYER_CSACTION_WAIT);
player->actor.world.pos.x = this->unk_6E8;
player->actor.world.pos.z = this->unk_6F0 + 410.0f;
player->actor.shape.rot.y = 0x7FFF;
player->actor.world.rot.y = player->actor.shape.rot.y;
Math_Vec3f_Copy(&this->subCamEye, &player->actor.world.pos);
this->subCamEye.y += 100.0f;
Math_Vec3f_Copy(&this->subCamAt, &this->actor.world.pos);
Play_EnableMotionBlur(150);
this->subCamFov = 60.0f;
boss = play->actorCtx.actorLists[ACTORCAT_BOSS].first;
while (boss != NULL) {
if (boss->id == ACTOR_EN_WATER_EFFECT) {
Actor_Kill(boss);
}
boss = boss->next;
}
}
} else {
this->unk_704 = 0;
}
break;
case 10:
if (this->unk_704 == 3) {
Actor_PlaySfx(&this->actor, NA_SE_EN_EYEGOLE_DEMO_EYE);
this->unk_74A = 1;
}
this->unk_2D0 = 10000.0f;
this->unk_2C8 = 300;
Math_ApproachF(&this->subCamFov, 20.0f, 0.3f, 11.0f);
if (this->unk_704 == 40) {
this->unk_708 = 11;
this->unk_704 = 0;
}
break;
case 11:
if (this->unk_704 > 50) {
this->unk_708 = 12;
this->unk_704 = 0;
this->actor.gravity = -3.0f;
}
break;
case 13:
if (this->unk_704 == 45) {
this->unk_708 = 1;
this->unk_704 = 0;
func_800B7298(play, &this->actor, PLAYER_CSACTION_21);
this->actor.gravity = 0.0f;
break;
}
case 12:
Actor_PlaySfx(&this->actor, NA_SE_EN_ME_ATTACK - SFX_FLAG);
Math_ApproachF(&this->subCamAt.x, this->actor.world.pos.x, 0.5f, 1000.0f);
Math_ApproachF(&this->subCamAt.y, this->actor.world.pos.y, 0.5f, 1000.0f);
Math_ApproachF(&this->subCamAt.z, this->actor.world.pos.z, 0.5f, 1000.0f);
if (this->actor.bgCheckFlags & BGCHECKFLAG_GROUND_TOUCH) {
Audio_PlaySfx(NA_SE_IT_BIG_BOMB_EXPLOSION);
this->unk_6F4 = 15;
this->unk_708 = 13;
this->unk_704 = 0;
this->unk_2DA = 10;
Actor_Spawn(&play->actorCtx, play, ACTOR_EN_CLEAR_TAG, this->actor.world.pos.x, this->actor.world.pos.y,
this->actor.world.pos.z, 0, 0, 0, CLEAR_TAG_PARAMS(CLEAR_TAG_SPLASH));
Actor_PlaySfx(&this->actor, NA_SE_EN_KONB_JUMP_LEV_OLD - SFX_FLAG);
this->subCamAtOscillator = 20;
}
break;
case 1:
player->actor.shape.rot.y = 0x7FFF;
player->actor.world.rot.y = player->actor.shape.rot.y;
Matrix_MultVecZ(-100.0f, &this->subCamEye);
this->subCamEye.x += player->actor.world.pos.x;
this->subCamEye.y = Player_GetHeight(player) + player->actor.world.pos.y + 36.0f;
this->subCamEye.z += player->actor.world.pos.z;
this->subCamAt.x = player->actor.world.pos.x;
this->subCamAt.y = (Player_GetHeight(player) + player->actor.world.pos.y) - 4.0f;
this->subCamAt.z = player->actor.world.pos.z;
if (this->unk_704 >= 35) {
this->unk_704 = 0;
this->unk_708 = 2;
this->unk_728 = -200.0f;
}
break;
case 2:
case 3:
Matrix_MultVecZ(500.0f, &this->subCamEye);
this->subCamEye.x += this->actor.world.pos.x;
this->subCamEye.y += this->actor.world.pos.y - 50.0f;
this->subCamEye.z += this->actor.world.pos.z;
this->subCamAt.x = this->actor.world.pos.x;
this->subCamAt.z = this->actor.world.pos.z;
this->subCamAt.y = (this->actor.world.pos.y - 70.0f) + this->unk_728;
Math_ApproachZeroF(&this->unk_728, 0.05f, this->unk_73C);
Math_ApproachF(&this->unk_73C, 3.0f, 1.0f, 0.05f);
if (this->unk_704 == 20) {
this->unk_708 = 3;
}
if (this->unk_704 == 70) {
this->unk_2C8 = 300;
this->unk_2D0 = 0.0f;
*D_809EE4D0_relocated = 1;
this->unk_2E2 = 60;
this->unk_2E0 = 93;
}
if (this->unk_704 > 140) {
Camera* mainCam = Play_GetCamera(play, CAM_ID_MAIN);
this->unk_708 = 0;
func_809ECD00(this, play);
mainCam->eye = this->subCamEye;
mainCam->eyeNext = this->subCamEye;
mainCam->at = this->subCamAt;
func_80169AFC(play, this->subCamId, 0);
this->subCamId = SUB_CAM_ID_DONE;
Cutscene_StopManual(play, &play->csCtx);
func_800B7298(play, &this->actor, PLAYER_CSACTION_END);
Play_DisableMotionBlur();
SET_EVENTINF(EVENTINF_60);
}
break;
}
if (this->subCamId != SUB_CAM_ID_DONE) {
Vec3f subCamAt;
ShrinkWindow_Letterbox_SetSizeTarget(27);
if (this->subCamAtOscillator != 0) {
this->subCamAtOscillator--;
}
Math_Vec3f_Copy(&subCamAt, &this->subCamAt);
subCamAt.y += Math_SinS(this->subCamAtOscillator * 0x4000) * this->subCamAtOscillator * 1.5f;
Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &this->subCamEye);
Play_SetCameraFov(play, this->subCamId, this->subCamFov);
Math_ApproachF(&this->subCamFov, 60.0f, 0.1f, 1.0f);
}
this->actor.shape.rot.y = this->actor.yawTowardsPlayer;
x = player->actor.world.pos.x - this->actor.world.pos.x;
y = player->actor.world.pos.y - this->actor.world.pos.y;
z = player->actor.world.pos.z - this->actor.world.pos.z;
this->actor.shape.rot.x = Math_Atan2S(-y, sqrtf(SQ(x) + SQ(z)));
}

View File

@ -58,13 +58,13 @@ s16 KaleidoScope_SetPageVertices(PlayState* play, Vtx* vtx, s16 vtxPage, s16 num
#define PIXEL_OFFSET ((1 << 4))
vtx[4 * row + 0].v.tc[0] = PIXEL_OFFSET;
vtx[4 * row + 0].v.tc[1] = (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 0].v.tc[1] = PIXEL_OFFSET;
vtx[4 * row + 1].v.tc[0] = PAGE_BG_WIDTH * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 1].v.tc[1] = (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 1].v.tc[1] = PIXEL_OFFSET;
vtx[4 * row + 2].v.tc[0] = PIXEL_OFFSET;
vtx[4 * row + 2].v.tc[1] = (cur_y - next_y + 1) * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 2].v.tc[1] = (cur_y - next_y) * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 3].v.tc[0] = PAGE_BG_WIDTH * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 3].v.tc[1] = (cur_y - next_y + 1) * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 3].v.tc[1] = (cur_y - next_y) * (1 << 5) + PIXEL_OFFSET;
vtx[4 * row + 0].v.cn[0] = vtx[4 * row + 1].v.cn[0] = vtx[4 * row + 2].v.cn[0] = vtx[4 * row + 3].v.cn[0] = 0;
vtx[4 * row + 0].v.cn[1] = vtx[4 * row + 1].v.cn[1] = vtx[4 * row + 2].v.cn[1] = vtx[4 * row + 3].v.cn[1] = 0;
@ -245,7 +245,7 @@ Gfx* KaleidoScope_DrawPageSections(Gfx* gfx, Vtx* vertices, TexturePtr* textures
G_IM_FMT_IA, G_IM_SIZ_8b, // fmt, siz
PAGE_BG_WIDTH + 2, PAGE_BG_HEIGHT + 2, // width, height
0, (bg_row + 0) * RECOMP_PAGE_ROW_HEIGHT, // uls, ult
PAGE_BG_WIDTH + 2, (bg_row + 1) * RECOMP_PAGE_ROW_HEIGHT + 2, // lrs, lrt
PAGE_BG_WIDTH + 2 - 1, (bg_row + 1) * RECOMP_PAGE_ROW_HEIGHT + 2 - 1, // lrs, lrt
0, // pal
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP,
G_TX_NOMASK, G_TX_NOMASK,
@ -253,8 +253,8 @@ Gfx* KaleidoScope_DrawPageSections(Gfx* gfx, Vtx* vertices, TexturePtr* textures
gDPSetTileSize(gfx++, G_TX_RENDERTILE,
0 << G_TEXTURE_IMAGE_FRAC,
0 << G_TEXTURE_IMAGE_FRAC,
(PAGE_BG_WIDTH + 2) <<G_TEXTURE_IMAGE_FRAC,
(RECOMP_PAGE_ROW_HEIGHT + 2) << G_TEXTURE_IMAGE_FRAC);
(PAGE_BG_WIDTH + 2 - 1) <<G_TEXTURE_IMAGE_FRAC,
(RECOMP_PAGE_ROW_HEIGHT + 2 - 1) << G_TEXTURE_IMAGE_FRAC);
gSPVertex(gfx++, vertices + 4 * bg_row, 4, 0);
gSP2Triangles(gfx++, 0, 3, 1, 0x0, 3, 0, 2, 0x0);
}

View File

@ -4,6 +4,7 @@
// Decomp rename, TODO update decomp and remove this
#define AudioVoice_GetWord func_801A5100
#include "z64voice.h"
#include "audiothread_cmd.h"
s32 func_80847190(PlayState* play, Player* this, s32 arg2);
s16 func_80832754(Player* this, s32 arg1);
@ -2297,3 +2298,437 @@ void draw_dpad_icons(PlayState* play) {
CLOSE_DISPS(play->state.gfxCtx);
}
typedef struct {
/* 0x0 */ s8 x;
/* 0x1 */ s8 y;
} OcarinaControlStick; // size = 0x2
typedef enum {
/* 0x0 */ SFX_CHANNEL_PLAYER0, // SfxPlayerBank
/* 0x1 */ SFX_CHANNEL_PLAYER1,
/* 0x2 */ SFX_CHANNEL_PLAYER2,
/* 0x3 */ SFX_CHANNEL_ITEM0, // SfxItemBank
/* 0x4 */ SFX_CHANNEL_ITEM1,
/* 0x5 */ SFX_CHANNEL_ENV0, // SfxEnvironmentBank
/* 0x6 */ SFX_CHANNEL_ENV1,
/* 0x7 */ SFX_CHANNEL_ENV2,
/* 0x8 */ SFX_CHANNEL_ENEMY0, // SfxEnemyBank
/* 0x9 */ SFX_CHANNEL_ENEMY1,
/* 0xA */ SFX_CHANNEL_ENEMY2,
/* 0xB */ SFX_CHANNEL_SYSTEM0, // SfxSystemBank
/* 0xC */ SFX_CHANNEL_SYSTEM1,
/* 0xD */ SFX_CHANNEL_OCARINA, // SfxOcarinaBank
/* 0xE */ SFX_CHANNEL_VOICE0, // SfxVoiceBank
/* 0xF */ SFX_CHANNEL_VOICE1
} SfxChannelIndex; // seqPlayerIndex = 2
extern u32 sOcarinaFlags;
extern u8 sOcarinaDropInputTimer;
extern u32 sOcarinaInputButtonStart;
extern u32 sOcarinaInputButtonCur;
extern u8 sCurOcarinaPitch;
extern u8 sCurOcarinaButtonIndex;
extern u32 sOcarinaInputButtonPrev;
extern s32 sOcarinaInputButtonPress;
extern u8 sRecordingState;
extern s8 sCurOcarinaBendIndex;
extern f32 sCurOcarinaBendFreq;
extern s8 sCurOcarinaVibrato;
extern OcarinaControlStick sOcarinaInputStickRel;
extern u8 sPrevOcarinaPitch;
extern f32 sDefaultOcarinaVolume;
extern s8 sOcarinaInstrumentId;
extern f32 AudioOcarina_BendPitchTwoSemitones(s8 bendIndex);
// @recomp Patch the function in order to read DPad inputs for the ocarina as well as CButton inputs.
void AudioOcarina_PlayControllerInput(u8 isOcarinaSfxSuppressedWhenCancelled) {
u32 ocarinaBtnsHeld;
// Prevents two different ocarina notes from being played on two consecutive frames
if ((sOcarinaFlags != 0) && (sOcarinaDropInputTimer != 0)) {
sOcarinaDropInputTimer--;
return;
}
// Ensures the button pressed to start the ocarina does not also play an ocarina note
// @recomp Check for DPad inputs as well.
if ((sOcarinaInputButtonStart == 0) ||
((sOcarinaInputButtonStart & (BTN_A | BTN_CRIGHT | BTN_CLEFT | BTN_CDOWN | BTN_CUP | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP)) !=
(sOcarinaInputButtonCur & (BTN_A | BTN_CRIGHT | BTN_CLEFT | BTN_CDOWN | BTN_CUP | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP)))) {
sOcarinaInputButtonStart = 0;
if (1) {}
sCurOcarinaPitch = OCARINA_PITCH_NONE;
sCurOcarinaButtonIndex = OCARINA_BTN_INVALID;
// @recomp Check for DPad inputs as well.
ocarinaBtnsHeld = (sOcarinaInputButtonCur & (BTN_A | BTN_CRIGHT | BTN_CLEFT | BTN_CDOWN | BTN_CUP | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP)) &
(sOcarinaInputButtonPrev & (BTN_A | BTN_CRIGHT | BTN_CLEFT | BTN_CDOWN | BTN_CUP | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP));
if (!(sOcarinaInputButtonPress & ocarinaBtnsHeld) && (sOcarinaInputButtonCur != 0)) {
sOcarinaInputButtonPress = sOcarinaInputButtonCur;
} else {
sOcarinaInputButtonPress &= ocarinaBtnsHeld;
}
// Interprets and transforms controller input into ocarina buttons and notes
if (CHECK_BTN_ANY(sOcarinaInputButtonPress, BTN_A)) {
sCurOcarinaPitch = OCARINA_PITCH_D4;
sCurOcarinaButtonIndex = OCARINA_BTN_A;
// @recomp Check for DPad down input as well.
} else if (CHECK_BTN_ANY(sOcarinaInputButtonPress, (BTN_CDOWN | BTN_DDOWN))) {
sCurOcarinaPitch = OCARINA_PITCH_F4;
sCurOcarinaButtonIndex = OCARINA_BTN_C_DOWN;
// @recomp Check for DPad right input as well.
} else if (CHECK_BTN_ANY(sOcarinaInputButtonPress, BTN_CRIGHT | BTN_DRIGHT)) {
sCurOcarinaPitch = OCARINA_PITCH_A4;
sCurOcarinaButtonIndex = OCARINA_BTN_C_RIGHT;
// @recomp Check for DPad left input as well.
} else if (CHECK_BTN_ANY(sOcarinaInputButtonPress, BTN_CLEFT | BTN_DLEFT)) {
sCurOcarinaPitch = OCARINA_PITCH_B4;
sCurOcarinaButtonIndex = OCARINA_BTN_C_LEFT;
// @recomp Check for DPad up input as well.
} else if (CHECK_BTN_ANY(sOcarinaInputButtonPress, BTN_CUP | BTN_DUP)) {
sCurOcarinaPitch = OCARINA_PITCH_D5;
sCurOcarinaButtonIndex = OCARINA_BTN_C_UP;
}
if (sOcarinaInputButtonCur) {}
// Pressing the R Button will raise the pitch by 1 semitone
if ((sCurOcarinaPitch != OCARINA_PITCH_NONE) && CHECK_BTN_ANY(sOcarinaInputButtonCur, BTN_R) &&
(sRecordingState != OCARINA_RECORD_SCARECROW_SPAWN)) {
sCurOcarinaButtonIndex += OCARINA_BUTTON_FLAG_BFLAT_RAISE; // Flag to resolve B Flat 4
sCurOcarinaPitch++; // Raise the pitch by 1 semitone
}
// Pressing the Z Button will lower the pitch by 1 semitone
if ((sCurOcarinaPitch != OCARINA_PITCH_NONE) && CHECK_BTN_ANY(sOcarinaInputButtonCur, BTN_Z) &&
(sRecordingState != OCARINA_RECORD_SCARECROW_SPAWN)) {
sCurOcarinaButtonIndex += OCARINA_BUTTON_FLAG_BFLAT_LOWER; // Flag to resolve B Flat 4
sCurOcarinaPitch--; // Lower the pitch by 1 semitone
}
if (sRecordingState != OCARINA_RECORD_SCARECROW_SPAWN) {
// Bend the pitch of the note based on y control stick
sCurOcarinaBendIndex = sOcarinaInputStickRel.y;
sCurOcarinaBendFreq = AudioOcarina_BendPitchTwoSemitones(sCurOcarinaBendIndex);
// Add vibrato of the ocarina note based on the x control stick
sCurOcarinaVibrato = ABS_ALT(sOcarinaInputStickRel.x) >> 2;
// Sets vibrato to io port 6
AUDIOCMD_CHANNEL_SET_IO(SEQ_PLAYER_SFX, SFX_CHANNEL_OCARINA, 6, sCurOcarinaVibrato);
} else {
// no bending or vibrato for recording state OCARINA_RECORD_SCARECROW_SPAWN
sCurOcarinaBendIndex = 0;
sCurOcarinaVibrato = 0;
sCurOcarinaBendFreq = 1.0f; // No bend
}
// Processes new and valid notes
if ((sCurOcarinaPitch != OCARINA_PITCH_NONE) && (sPrevOcarinaPitch != sCurOcarinaPitch)) {
// Sets ocarina instrument Id to io port 7, which is used
// as an index in seq 0 to get the true instrument Id
AUDIOCMD_CHANNEL_SET_IO(SEQ_PLAYER_SFX, SFX_CHANNEL_OCARINA, 7, sOcarinaInstrumentId - 1);
// Sets pitch to io port 5
AUDIOCMD_CHANNEL_SET_IO(SEQ_PLAYER_SFX, SFX_CHANNEL_OCARINA, 5, sCurOcarinaPitch);
AudioSfx_PlaySfx(NA_SE_OC_OCARINA, &gSfxDefaultPos, 4, &sCurOcarinaBendFreq, &sDefaultOcarinaVolume,
&gSfxDefaultReverb);
} else if ((sPrevOcarinaPitch != OCARINA_PITCH_NONE) && (sCurOcarinaPitch == OCARINA_PITCH_NONE) &&
!isOcarinaSfxSuppressedWhenCancelled) {
// Stops ocarina sound when transitioning from playing to not playing a note
AudioSfx_StopById(NA_SE_OC_OCARINA);
}
}
}
extern u8 sOcarinaHasStartedSong;
extern u8 sOcarinaStaffPlayingPos;
extern u8 sCurOcarinaSongWithoutMusicStaff[8];
extern u8 sOcarinaWithoutMusicStaffPos;
extern u8 sFirstOcarinaSongIndex;
extern u32 sOcarinaAvailableSongFlags;
extern u8 sLastOcarinaSongIndex;
extern u8 sButtonToPitchMap[5];
extern u8 sPlayedOcarinaSongIndexPlusOne;
extern u8 sIsOcarinaInputEnabled;
extern void AudioOcarina_CheckIfStartedSong(void);
extern void AudioOcarina_UpdateCurOcarinaSong(void);
// @recomp Patch the L button check (for free ocarina playing) to account for DPad ocarina.
void AudioOcarina_CheckSongsWithoutMusicStaff(void) {
u32 pitch;
u8 ocarinaStaffPlayingPosStart;
u8 songIndex;
u8 j;
u8 k;
// @recomp Add the DPad inputs to the check.
if (CHECK_BTN_ANY(sOcarinaInputButtonCur, BTN_L) &&
CHECK_BTN_ANY(sOcarinaInputButtonCur, BTN_A | BTN_CRIGHT | BTN_CLEFT | BTN_CDOWN | BTN_CUP | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP)) {
AudioOcarina_StartDefault(sOcarinaFlags);
return;
}
AudioOcarina_CheckIfStartedSong();
if (!sOcarinaHasStartedSong) {
return;
}
ocarinaStaffPlayingPosStart = sOcarinaStaffPlayingPos;
if ((sPrevOcarinaPitch != sCurOcarinaPitch) && (sCurOcarinaPitch != OCARINA_PITCH_NONE)) {
sOcarinaStaffPlayingPos++;
if (sOcarinaStaffPlayingPos > ARRAY_COUNT(sCurOcarinaSongWithoutMusicStaff)) {
sOcarinaStaffPlayingPos = 1;
}
AudioOcarina_UpdateCurOcarinaSong();
if ((ABS_ALT(sCurOcarinaBendIndex) > 20) && (ocarinaStaffPlayingPosStart != sOcarinaStaffPlayingPos)) {
sCurOcarinaSongWithoutMusicStaff[sOcarinaWithoutMusicStaffPos - 1] = OCARINA_PITCH_NONE;
} else {
sCurOcarinaSongWithoutMusicStaff[sOcarinaWithoutMusicStaffPos - 1] = sCurOcarinaPitch;
}
// This nested for-loop tests to see if the notes from the ocarina are identical
// to any of the songIndex from sFirstOcarinaSongIndex to sLastOcarinaSongIndex
// Loop through each of the songs
for (songIndex = sFirstOcarinaSongIndex; songIndex < sLastOcarinaSongIndex; songIndex++) {
// Checks to see if the song is available to be played
if ((u32)sOcarinaAvailableSongFlags & (1 << songIndex)) {
// Loops through all possible starting indices?
// Loops through the notes of the song?
for (j = 0, k = 0; (j < gOcarinaSongButtons[songIndex].numButtons) && (k == 0) &&
(sOcarinaWithoutMusicStaffPos >= gOcarinaSongButtons[songIndex].numButtons);) {
pitch = sCurOcarinaSongWithoutMusicStaff[(sOcarinaWithoutMusicStaffPos -
gOcarinaSongButtons[songIndex].numButtons) +
j];
if (pitch == sButtonToPitchMap[gOcarinaSongButtons[songIndex].buttonIndex[j]]) {
j++;
} else {
k++;
}
}
// This conditional is true if songIndex = i is detected
if (j == gOcarinaSongButtons[songIndex].numButtons) {
sPlayedOcarinaSongIndexPlusOne = songIndex + 1;
sIsOcarinaInputEnabled = false;
sOcarinaFlags = 0;
}
}
}
}
}
extern s32 Player_GetMovementSpeedAndYaw(Player* this, f32* outSpeedTarget, s16* outYawTarget, f32 speedMode,
PlayState* play);
extern bool get_analog_cam_active();
extern void skip_analog_cam_once();
// @recomp Updates yaw while inside of deku flower.
void func_80855F9C(PlayState* play, Player* this) {
f32 speedTarget;
s16 yawTarget;
this->stateFlags2 |= PLAYER_STATE2_20;
Player_GetMovementSpeedAndYaw(this, &speedTarget, &yawTarget, 0.018f, play);
// @recomp If left stick inputs are occurring, prevent analog cam.
if ((play->state.input[0].rel.stick_y != 0 || play->state.input[0].rel.stick_x != 0)) {
skip_analog_cam_once();
}
if (get_analog_cam_active()) {
// @recomp set current yaw to active camera's yaw.
this->currentYaw = Camera_GetInputDirYaw(GET_ACTIVE_CAM(play));
} else {
Math_ScaledStepToS(&this->currentYaw, yawTarget, 0x258);
}
}
extern void set_analog_cam_active(bool isActive);
extern void Player_Action_4(Player* this, PlayState* play);
extern s32 Player_SetAction(PlayState* play, Player* this, PlayerActionFunc actionFunc, s32 arg3);
extern LinkAnimationHeader gPlayerAnim_pg_maru_change;
s32 func_80857950(PlayState* play, Player* this) {
// @recomp track if newly going from non-spike roll to spike roll (spike rolling when this->unk_B86[1] == 1)
static bool wasOff = true;
bool isOff = this->unk_B86[1] == 0;
if (wasOff && !isOff) {
// @recomp set analog cam to be active now that rolling has started
set_analog_cam_active(false);
}
wasOff = isOff;
// @recomp Manual relocation, TODO remove when automated.
Input* player_control_input = *(Input**)KaleidoManager_GetRamAddr(&sPlayerControlInput);
if (((this->unk_B86[1] == 0) && !CHECK_BTN_ALL(player_control_input->cur.button, BTN_A)) ||
((this->av1.actionVar1 == 3) && (this->actor.velocity.y < 0.0f))) {
// @recomp Manual relocation, TODO remove when automated.
PlayerActionFunc Player_Action_4_reloc = KaleidoManager_GetRamAddr(Player_Action_4);
Player_SetAction(play, this, Player_Action_4_reloc, 1);
Math_Vec3f_Copy(&this->actor.world.pos, &this->actor.prevPos);
PlayerAnimation_Change(play, &this->skelAnime, &gPlayerAnim_pg_maru_change, -2.0f / 3.0f, 7.0f, 0.0f,
ANIMMODE_ONCE, 0.0f);
Player_PlaySfx(this, NA_SE_PL_BALL_TO_GORON);
wasOff = true;
return true;
}
return false;
}
typedef PlayerAnimationHeader* D_8085BE84_t[PLAYER_ANIMTYPE_MAX];
extern PlayerAnimationHeader* D_8085BE84[PLAYER_ANIMGROUP_MAX][PLAYER_ANIMTYPE_MAX];
extern LinkAnimationHeader gPlayerAnim_link_normal_backspace;
extern s32 func_80832F24(Player* this);
extern s32 func_8083FE38(Player* this, PlayState* play);
extern s32 Player_ActionChange_11(Player* this, PlayState* play);
extern void func_8083A98C(Actor* thisx, PlayState* play2);
extern void func_80836A98(Player* this, PlayerAnimationHeader* anim, PlayState* play);
extern void func_80830B38(Player* this);
extern void Player_AnimationPlayLoop(PlayState* play, Player* this, PlayerAnimationHeader* anim);
extern s32 Player_UpdateUpperBody(Player* this, PlayState* play);
extern void func_8082F164(Player* this, u16 button);
extern s32 func_808401F4(PlayState* play, Player* this);
extern void func_8082FA5C(PlayState* play, Player* this, PlayerMeleeWeaponState meleeWeaponState);
extern s32 func_8083FD80(Player* this, PlayState* play);
extern void func_8082DC38(Player* this);
extern void func_80836A5C(Player* this, PlayState* play);
// @recomp Patch the shielding function to respect the aiming axis inversion setting.
void Player_Action_18(Player* this, PlayState* play) {
//@recomp Manual relocation. TODO remove when automated
D_8085BE84_t* D_8085BE84_reloc = (D_8085BE84_t*)KaleidoManager_GetRamAddr(D_8085BE84);
Input* sPlayerControlInput_reloc = *(Input**)KaleidoManager_GetRamAddr(&sPlayerControlInput);
func_80832F24(this);
if (this->transformation == PLAYER_FORM_GORON) {
SkelAnime_Update(&this->unk_2C8);
if (!func_8083FE38(this, play)) {
if (!Player_ActionChange_11(this, play)) {
this->stateFlags1 &= ~PLAYER_STATE1_400000;
if (this->itemAction <= PLAYER_IA_MINUS1) {
func_80123C58(this);
}
func_80836A98(this, D_8085BE84_reloc[PLAYER_ANIMGROUP_defense_end][this->modelAnimType], play);
func_80830B38(this);
} else {
this->stateFlags1 |= PLAYER_STATE1_400000;
}
}
return;
}
if (PlayerAnimation_Update(play, &this->skelAnime)) {
if (!Player_IsGoronOrDeku(this)) {
Player_AnimationPlayLoop(play, this, D_8085BE84_reloc[PLAYER_ANIMGROUP_defense_wait][this->modelAnimType]);
}
this->av2.actionVar2 = 1;
this->av1.actionVar1 = 0;
}
if (!Player_IsGoronOrDeku(this)) {
this->stateFlags1 |= PLAYER_STATE1_400000;
Player_UpdateUpperBody(this, play);
this->stateFlags1 &= ~PLAYER_STATE1_400000;
if (this->transformation == PLAYER_FORM_ZORA) {
func_8082F164(this, BTN_R | BTN_B);
}
}
if (this->av2.actionVar2 != 0) {
f32 yStick = sPlayerControlInput_reloc->rel.stick_y * 180;
f32 xStick = sPlayerControlInput_reloc->rel.stick_x * -120;
s16 temp_a0 = this->actor.shape.rot.y - Camera_GetInputDirYaw(GET_ACTIVE_CAM(play));
s16 var_a1;
s16 temp_ft5;
s16 var_a2;
s16 var_a3;
// @recomp Get the aiming camera inversion state.
s32 inverted_x, inverted_y;
recomp_get_inverted_axes(&inverted_x, &inverted_y);
// @recomp Invert the Y and X stick values based on the inverted aiming setting.
if (!inverted_y) {
yStick = -yStick;
}
if (inverted_x) {
xStick = -xStick;
}
var_a1 = (yStick * Math_CosS(temp_a0)) + (Math_SinS(temp_a0) * xStick);
temp_ft5 = (xStick * Math_CosS(temp_a0)) - (Math_SinS(temp_a0) * yStick);
var_a1 = CLAMP_MAX(var_a1, 0xDAC);
var_a2 = ABS_ALT(var_a1 - this->actor.focus.rot.x) * 0.25f;
var_a2 = CLAMP_MIN(var_a2, 0x64);
var_a3 = ABS_ALT(temp_ft5 - this->upperLimbRot.y) * 0.25f;
var_a3 = CLAMP_MIN(var_a3, 0x32);
Math_ScaledStepToS(&this->actor.focus.rot.x, var_a1, var_a2);
this->upperLimbRot.x = this->actor.focus.rot.x;
Math_ScaledStepToS(&this->upperLimbRot.y, temp_ft5, var_a3);
if (this->av1.actionVar1 != 0) {
if (!func_808401F4(play, this)) {
if (this->skelAnime.curFrame < 2.0f) {
func_8082FA5C(play, this, PLAYER_MELEE_WEAPON_STATE_1);
}
} else {
this->av2.actionVar2 = 1;
this->av1.actionVar1 = 0;
}
} else if (!func_8083FE38(this, play)) {
if (Player_ActionChange_11(this, play)) {
func_8083FD80(this, play);
} else {
this->stateFlags1 &= ~PLAYER_STATE1_400000;
func_8082DC38(this);
if (Player_IsGoronOrDeku(this)) {
func_80836A5C(this, play);
PlayerAnimation_Change(play, &this->skelAnime, this->skelAnime.animation, 1.0f,
Animation_GetLastFrame(this->skelAnime.animation), 0.0f, 2, 0.0f);
} else {
if (this->itemAction <= PLAYER_IA_MINUS1) {
func_80123C58(this);
}
func_80836A98(this, D_8085BE84_reloc[PLAYER_ANIMGROUP_defense_end][this->modelAnimType], play);
}
Player_PlaySfx(this, NA_SE_IT_SHIELD_REMOVE);
return;
}
} else {
return;
}
}
this->stateFlags1 |= PLAYER_STATE1_400000;
Player_SetModelsForHoldingShield(this);
this->unk_AA6 |= 0xC1;
}

View File

@ -4,7 +4,7 @@
#ifdef MIPS
#include "ultra64.h"
#else
#include "recomp.h"
#include "librecomp/recomp.h"
#endif
#ifdef __cplusplus

View File

@ -14,6 +14,7 @@ D_808DE5B0 = 0x808DE5B0;
sHappyMaskSalesmanAnimationInfo = 0x80AD22C0;
D_808890F0 = 0x808890F0;
D_8088911C = 0x8088911C;
D_809EE4D0 = 0x809EE4D0;
/* Dummy addresses that get recompiled into function calls */
recomp_puts = 0x8F000000;

View File

@ -1,7 +1,9 @@
#include "recomp_config.h"
#include "zelda_config.h"
#include "recomp_input.h"
#include "recomp_sound.h"
#include "../../ultramodern/config.hpp"
#include "zelda_sound.h"
#include "zelda_render.h"
#include "ultramodern/config.hpp"
#include "librecomp/files.hpp"
#include <filesystem>
#include <fstream>
#include <iomanip>
@ -17,22 +19,23 @@ constexpr std::u8string_view general_filename = u8"general.json";
constexpr std::u8string_view graphics_filename = u8"graphics.json";
constexpr std::u8string_view controls_filename = u8"controls.json";
constexpr std::u8string_view sound_filename = u8"sound.json";
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr auto res_default = ultramodern::Resolution::Auto;
constexpr auto hr_default = ultramodern::HUDRatioMode::Clamp16x9;
constexpr auto api_default = ultramodern::GraphicsApi::Auto;
constexpr auto ar_default = RT64::UserConfiguration::AspectRatio::Expand;
constexpr auto msaa_default = RT64::UserConfiguration::Antialiasing::MSAA2X;
constexpr auto rr_default = RT64::UserConfiguration::RefreshRate::Display;
constexpr auto hpfb_default = ultramodern::HighPrecisionFramebuffer::Auto;
constexpr auto res_default = ultramodern::renderer::Resolution::Auto;
constexpr auto hr_default = ultramodern::renderer::HUDRatioMode::Clamp16x9;
constexpr auto api_default = ultramodern::renderer::GraphicsApi::Auto;
constexpr auto ar_default = ultramodern::renderer::AspectRatio::Expand;
constexpr auto msaa_default = ultramodern::renderer::Antialiasing::MSAA2X;
constexpr auto rr_default = ultramodern::renderer::RefreshRate::Display;
constexpr auto hpfb_default = ultramodern::renderer::HighPrecisionFramebuffer::Auto;
constexpr int ds_default = 1;
constexpr int rr_manual_default = 60;
constexpr bool developer_mode_default = false;
static bool is_steam_deck = false;
ultramodern::WindowMode wm_default() {
return is_steam_deck ? ultramodern::WindowMode::Fullscreen : ultramodern::WindowMode::Windowed;
ultramodern::renderer::WindowMode wm_default() {
return is_steam_deck ? ultramodern::renderer::WindowMode::Fullscreen : ultramodern::renderer::WindowMode::Windowed;
}
#ifdef __gnu_linux__
@ -84,7 +87,7 @@ void call_if_key_exists(void (*func)(T), const json& j, const std::string& key)
}
namespace ultramodern {
void to_json(json& j, const GraphicsConfig& config) {
void to_json(json& j, const renderer::GraphicsConfig& config) {
j = json{
{"res_option", config.res_option},
{"wm_option", config.wm_option},
@ -100,7 +103,7 @@ namespace ultramodern {
};
}
void from_json(const json& j, GraphicsConfig& config) {
void from_json(const json& j, renderer::GraphicsConfig& config) {
config.res_option = from_or_default(j, "res_option", res_default);
config.wm_option = from_or_default(j, "wm_option", wm_default());
config.hr_option = from_or_default(j, "hr_option", hr_default);
@ -126,7 +129,12 @@ namespace recomp {
}
}
std::filesystem::path recomp::get_app_folder_path() {
std::filesystem::path zelda64::get_app_folder_path() {
// directly check for portable.txt (windows and native linux binary)
if (std::filesystem::exists("portable.txt")) {
return std::filesystem::current_path();
}
std::filesystem::path recomp_dir{};
#if defined(_WIN32)
@ -134,11 +142,16 @@ std::filesystem::path recomp::get_app_folder_path() {
PWSTR known_path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &known_path);
if (result == S_OK) {
recomp_dir = std::filesystem::path{known_path} / recomp::program_id;
recomp_dir = std::filesystem::path{known_path} / zelda64::program_id;
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
@ -146,53 +159,94 @@ std::filesystem::path recomp::get_app_folder_path() {
}
if (homedir != nullptr) {
recomp_dir = std::filesystem::path{homedir} / (std::u8string{u8".config/"} + std::u8string{recomp::program_id});
recomp_dir = std::filesystem::path{homedir} / (std::u8string{u8".config/"} + std::u8string{zelda64::program_id});
}
#endif
return recomp_dir;
}
void save_general_config(const std::filesystem::path& path) {
std::ofstream config_file{path};
bool read_json(std::ifstream input_file, nlohmann::json& json_out) {
if (!input_file.good()) {
return false;
}
try {
input_file >> json_out;
}
catch (nlohmann::json::parse_error&) {
return false;
}
return true;
}
bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& json_out) {
// Try reading and parsing the base file.
if (read_json(std::ifstream{path}, json_out)) {
return true;
}
// Try reading and parsing the backup file.
if (read_json(recomp::open_input_backup_file(path), json_out)) {
return true;
}
// Both reads failed.
return false;
}
bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) {
{
std::ofstream output_file = recomp::open_output_file_with_backup(path);
if (!output_file.good()) {
return false;
}
output_file << std::setw(4) << json_data;
}
return recomp::finalize_output_file_with_backup(path);
}
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
recomp::to_json(config_json["targeting_mode"], recomp::get_targeting_mode());
zelda64::to_json(config_json["targeting_mode"], zelda64::get_targeting_mode());
recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode());
config_json["rumble_strength"] = recomp::get_rumble_strength();
config_json["gyro_sensitivity"] = recomp::get_gyro_sensitivity();
config_json["mouse_sensitivity"] = recomp::get_mouse_sensitivity();
config_json["joystick_deadzone"] = recomp::get_joystick_deadzone();
config_json["autosave_mode"] = recomp::get_autosave_mode();
config_json["camera_invert_mode"] = recomp::get_camera_invert_mode();
config_json["analog_cam_mode"] = recomp::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = recomp::get_analog_camera_invert_mode();
config_json["debug_mode"] = recomp::get_debug_mode_enabled();
config_file << std::setw(4) << config_json;
config_json["autosave_mode"] = zelda64::get_autosave_mode();
config_json["camera_invert_mode"] = zelda64::get_camera_invert_mode();
config_json["analog_cam_mode"] = zelda64::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = zelda64::get_analog_camera_invert_mode();
config_json["debug_mode"] = zelda64::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
void set_general_settings_from_json(const nlohmann::json& config_json) {
recomp::set_targeting_mode(from_or_default(config_json, "targeting_mode", recomp::TargetingMode::Switch));
zelda64::set_targeting_mode(from_or_default(config_json, "targeting_mode", zelda64::TargetingMode::Switch));
recomp::set_background_input_mode(from_or_default(config_json, "background_input_mode", recomp::BackgroundInputMode::On));
recomp::set_rumble_strength(from_or_default(config_json, "rumble_strength", 25));
recomp::set_gyro_sensitivity(from_or_default(config_json, "gyro_sensitivity", 50));
recomp::set_mouse_sensitivity(from_or_default(config_json, "mouse_sensitivity", is_steam_deck ? 50 : 0));
recomp::set_joystick_deadzone(from_or_default(config_json, "joystick_deadzone", 5));
recomp::set_autosave_mode(from_or_default(config_json, "autosave_mode", recomp::AutosaveMode::On));
recomp::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", recomp::CameraInvertMode::InvertY));
recomp::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", recomp::AnalogCamMode::Off));
recomp::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", recomp::CameraInvertMode::InvertNone));
recomp::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
zelda64::set_autosave_mode(from_or_default(config_json, "autosave_mode", zelda64::AutosaveMode::On));
zelda64::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", zelda64::CameraInvertMode::InvertY));
zelda64::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", zelda64::AnalogCamMode::Off));
zelda64::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", zelda64::CameraInvertMode::InvertNone));
zelda64::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
}
void load_general_config(const std::filesystem::path& path) {
std::ifstream config_file{path};
bool load_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_file >> config_json;
if (!read_json_with_backups(path, config_json)) {
return false;
}
set_general_settings_from_json(config_json);
return true;
}
void assign_mapping(recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& value) {
@ -235,21 +289,21 @@ void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Map
assign_mapping_complete(device, recomp::GameInput::TOGGLE_MENU, values.toggle_menu);
};
void recomp::reset_input_bindings() {
void zelda64::reset_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void recomp::reset_cont_input_bindings() {
void zelda64::reset_cont_input_bindings() {
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void recomp::reset_kb_input_bindings() {
void zelda64::reset_kb_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
}
void reset_graphics_options() {
ultramodern::GraphicsConfig new_config{};
ultramodern::renderer::GraphicsConfig new_config{};
new_config.res_option = res_default;
new_config.wm_option = wm_default();
new_config.hr_option = hr_default;
@ -260,26 +314,25 @@ void reset_graphics_options() {
new_config.hpfb_option = hpfb_default;
new_config.rr_manual_value = rr_manual_default;
new_config.developer_mode = developer_mode_default;
ultramodern::set_graphics_config(new_config);
ultramodern::renderer::set_graphics_config(new_config);
}
void save_graphics_config(const std::filesystem::path& path) {
std::ofstream config_file{path};
bool save_graphics_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
ultramodern::to_json(config_json, ultramodern::get_graphics_config());
config_file << std::setw(4) << config_json;
ultramodern::to_json(config_json, ultramodern::renderer::get_graphics_config());
return save_json_with_backups(path, config_json);
}
void load_graphics_config(const std::filesystem::path& path) {
std::ifstream config_file{path};
bool load_graphics_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
config_file >> config_json;
ultramodern::GraphicsConfig new_config{};
ultramodern::renderer::GraphicsConfig new_config{};
ultramodern::from_json(config_json, new_config);
ultramodern::set_graphics_config(new_config);
ultramodern::renderer::set_graphics_config(new_config);
return true;
}
void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::InputDevice device) {
@ -291,7 +344,7 @@ void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::In
}
};
void save_controls_config(const std::filesystem::path& path) {
bool save_controls_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_json["keyboard"] = {};
@ -304,8 +357,7 @@ void save_controls_config(const std::filesystem::path& path) {
add_input_bindings(config_json["controller"], cur_input, recomp::InputDevice::Controller);
}
std::ofstream config_file{path};
config_file << std::setw(4) << config_json;
return save_json_with_backups(path, config_json);
}
bool load_input_device_from_json(const nlohmann::json& config_json, recomp::InputDevice device, const std::string& key) {
@ -329,8 +381,8 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu
cur_input,
recomp::get_default_mapping_for_input(
device == recomp::InputDevice::Keyboard ?
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
cur_input
)
);
@ -349,11 +401,11 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu
return true;
}
void load_controls_config(const std::filesystem::path& path) {
std::ifstream config_file{path};
bool load_controls_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_file >> config_json;
if (!read_json_with_backups(path, config_json)) {
return false;
}
if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
@ -362,35 +414,36 @@ void load_controls_config(const std::filesystem::path& path) {
if (!load_input_device_from_json(config_json, recomp::InputDevice::Controller, "controller")) {
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
return true;
}
void save_sound_config(const std::filesystem::path& path) {
bool save_sound_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_json["main_volume"] = recomp::get_main_volume();
config_json["bgm_volume"] = recomp::get_bgm_volume();
config_json["low_health_beeps"] = recomp::get_low_health_beeps_enabled();
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
std::ofstream config_file{path};
config_file << std::setw(4) << config_json;
return save_json_with_backups(path, config_json);
}
void load_sound_config(const std::filesystem::path& path) {
std::ifstream config_file{path};
bool load_sound_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
config_file >> config_json;
recomp::reset_sound_settings();
call_if_key_exists(recomp::set_main_volume, config_json, "main_volume");
call_if_key_exists(recomp::set_bgm_volume, config_json, "bgm_volume");
call_if_key_exists(recomp::set_low_health_beeps_enabled, config_json, "low_health_beeps");
zelda64::reset_sound_settings();
call_if_key_exists(zelda64::set_main_volume, config_json, "main_volume");
call_if_key_exists(zelda64::set_bgm_volume, config_json, "bgm_volume");
call_if_key_exists(zelda64::set_low_health_beeps_enabled, config_json, "low_health_beeps");
return true;
}
void recomp::load_config() {
void zelda64::load_config() {
detect_steam_deck();
std::filesystem::path recomp_dir = recomp::get_app_folder_path();
std::filesystem::path recomp_dir = zelda64::get_app_folder_path();
std::filesystem::path general_path = recomp_dir / general_filename;
std::filesystem::path graphics_path = recomp_dir / graphics_filename;
std::filesystem::path controls_path = recomp_dir / controls_filename;
@ -400,48 +453,40 @@ void recomp::load_config() {
std::filesystem::create_directories(recomp_dir);
}
if (std::filesystem::exists(general_path)) {
load_general_config(general_path);
}
else {
// TODO error handling for failing to save config files after resetting them.
if (!load_general_config(general_path)) {
// Set the general settings from an empty json to use defaults.
set_general_settings_from_json({});
save_general_config(general_path);
}
if (std::filesystem::exists(graphics_path)) {
load_graphics_config(graphics_path);
}
else {
if (!load_graphics_config(graphics_path)) {
reset_graphics_options();
save_graphics_config(graphics_path);
}
if (std::filesystem::exists(controls_path)) {
load_controls_config(controls_path);
}
else {
recomp::reset_input_bindings();
if (!load_controls_config(controls_path)) {
zelda64::reset_input_bindings();
save_controls_config(controls_path);
}
if (std::filesystem::exists(sound_path)) {
load_sound_config(sound_path);
}
else {
recomp::reset_sound_settings();
if (!load_sound_config(sound_path)) {
zelda64::reset_sound_settings();
save_sound_config(sound_path);
}
}
void recomp::save_config() {
std::filesystem::path recomp_dir = recomp::get_app_folder_path();
void zelda64::save_config() {
std::filesystem::path recomp_dir = zelda64::get_app_folder_path();
if (recomp_dir.empty()) {
return;
}
std::filesystem::create_directories(recomp_dir);
// TODO error handling for failing to save config files.
save_general_config(recomp_dir / general_filename);
save_graphics_config(recomp_dir / graphics_filename);

View File

@ -1,8 +1,8 @@
#include <array>
#include "recomp_helpers.h"
#include "librecomp/helpers.hpp"
#include "recomp_input.h"
#include "../ultramodern/ultramodern.hpp"
#include "ultramodern/ultramodern.hpp"
// Arrays that hold the mappings for every input for keyboard and controller respectively.
using input_mapping = std::array<recomp::InputField, recomp::bindings_per_input>;
@ -75,10 +75,14 @@ void recomp::set_input_binding(recomp::GameInput input, size_t binding_index, re
}
}
void recomp::get_n64_input(uint16_t* buttons_out, float* x_out, float* y_out) {
bool recomp::get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out) {
uint16_t cur_buttons = 0;
float cur_x = 0.0f;
float cur_y = 0.0f;
if (controller_num != 0) {
return false;
}
if (!recomp::game_input_disabled()) {
for (size_t i = 0; i < n64_button_values.size(); i++) {
@ -107,4 +111,6 @@ void recomp::get_n64_input(uint16_t* buttons_out, float* x_out, float* y_out) {
*buttons_out = cur_buttons;
*x_out = std::clamp(cur_x, -1.0f, 1.0f);
*y_out = std::clamp(cur_y, -1.0f, 1.0f);
return true;
}

View File

@ -1,13 +1,13 @@
#include <atomic>
#include "recomp_debug.h"
#include "recomp_helpers.h"
#include "zelda_debug.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
std::atomic<uint16_t> pending_warp = 0xFFFF;
std::atomic<uint32_t> pending_set_time = 0xFFFF;
void recomp::do_warp(int area, int scene, int entrance) {
const recomp::SceneWarps game_scene = recomp::game_warps[area].scenes[scene];
void zelda64::do_warp(int area, int scene, int entrance) {
const zelda64::SceneWarps game_scene = zelda64::game_warps[area].scenes[scene];
int game_scene_index = game_scene.index;
pending_warp.store(((game_scene_index & 0xFF) << 8) | ((entrance & 0x0F) << 4));
}
@ -17,7 +17,7 @@ extern "C" void recomp_get_pending_warp(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, pending_warp.exchange(0xFFFF));
}
void recomp::set_time(uint8_t day, uint8_t hour, uint8_t minute) {
void zelda64::set_time(uint8_t day, uint8_t hour, uint8_t minute) {
pending_set_time.store((day << 16) | (uint16_t(hour) << 8) | minute);
}

View File

@ -1,12 +1,12 @@
#include <atomic>
#include <mutex>
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
#include "ultramodern/ultramodern.hpp"
#include "librecomp/recomp.h"
#include "recomp_input.h"
#include "zelda_config.h"
#include "recomp_ui.h"
#include "SDL.h"
#include "rt64_layer.h"
#include "promptfont.h"
#include "GamepadMotion.hpp"
@ -75,13 +75,13 @@ void recomp::stop_scanning_input() {
void queue_if_enabled(SDL_Event* event) {
if (!recomp::all_input_disabled()) {
recomp::queue_event(*event);
recompui::queue_event(*event);
}
}
static std::atomic_bool cursor_enabled = true;
void recomp::set_cursor_visible(bool visible) {
void recompui::set_cursor_visible(bool visible) {
cursor_enabled.store(visible);
}
@ -102,15 +102,16 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
{
SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events.
if (event->key.repeat) {
// Skip repeated events when not in the menu
if (recompui::get_current_menu() == recompui::Menu::None &&
event->key.repeat) {
break;
}
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
) {
recomp::toggle_fullscreen();
recompui::toggle_fullscreen();
}
if (scanning_device != recomp::InputDevice::COUNT) {
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
@ -155,12 +156,12 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
return true;
}
if (recomp::get_current_menu() != recomp::Menu::Config) {
recomp::set_current_menu(recomp::Menu::Config);
if (recompui::get_current_menu() != recompui::Menu::Config) {
recompui::set_current_menu(recompui::Menu::Config);
}
recomp::open_quit_game_prompt();
recomp::activate_mouse();
zelda64::open_quit_game_prompt();
recompui::activate_mouse();
break;
}
case SDL_EventType::SDL_MOUSEWHEEL:
@ -435,18 +436,35 @@ void recomp::poll_inputs() {
bool save_is_held = InputState.keys[SDL_SCANCODE_F5] != 0;
bool load_is_held = InputState.keys[SDL_SCANCODE_F7] != 0;
if (save_is_held && !save_was_held) {
recomp::quicksave_save();
zelda64::quicksave_save();
}
else if (load_is_held && !load_was_held) {
recomp::quicksave_load();
zelda64::quicksave_load();
}
save_was_held = save_is_held;
}
#endif
}
void recomp::set_rumble(bool on) {
InputState.rumble_active = on;
void recomp::set_rumble(int controller_num, bool on) {
if (controller_num == 0) {
InputState.rumble_active = on;
}
}
ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) {
switch (controller_num) {
case 0:
return ultramodern::input::connected_device_info_t {
.connected_device = ultramodern::input::Device::Controller,
.connected_pak = ultramodern::input::Pak::RumblePak,
};
}
return ultramodern::input::connected_device_info_t {
.connected_device = ultramodern::input::Device::None,
.connected_pak = ultramodern::input::Pak::None,
};
}
static float smoothstep(float from, float to, float amount) {
@ -649,7 +667,7 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
bool recomp::game_input_disabled() {
// Disable input if any menu is open.
return recomp::get_current_menu() != recomp::Menu::None;
return recompui::get_current_menu() != recompui::Menu::None;
}
bool recomp::all_input_disabled() {

View File

@ -2,9 +2,9 @@
#if 0
#include "recomp_helpers.h"
#include "recomp_input.h"
#include "../ultramodern/ultramodern.hpp"
#include "librecomp/helpers.hpp"
#include "librecomp/input.hpp"
#include "ultramodern/ultramodern.hpp"
enum class QuicksaveAction {
None,
@ -14,11 +14,11 @@ enum class QuicksaveAction {
std::atomic<QuicksaveAction> cur_quicksave_action = QuicksaveAction::None;
void recomp::quicksave_save() {
void zelda64::quicksave_save() {
cur_quicksave_action.store(QuicksaveAction::Save);
}
void recomp::quicksave_load() {
void zelda64::quicksave_load() {
cur_quicksave_action.store(QuicksaveAction::Load);
}

View File

@ -1,18 +1,18 @@
#include <cmath>
#include "recomp.h"
#include "recomp_overlays.h"
#include "recomp_config.h"
#include "librecomp/recomp.h"
#include "librecomp/overlays.hpp"
#include "zelda_config.h"
#include "recomp_input.h"
#include "recomp_ui.h"
#include "recomp_sound.h"
#include "recomp_helpers.h"
#include "rt64_layer.h"
#include "zelda_render.h"
#include "zelda_sound.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
#include "../patches/graphics.h"
#include "../patches/sound.h"
#include "../ultramodern/ultramodern.hpp"
#include "../ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
extern "C" void recomp_update_inputs(uint8_t* rdram, recomp_context* ctx) {
recomp::poll_inputs();
@ -59,32 +59,32 @@ extern "C" void recomp_get_target_framerate(uint8_t* rdram, recomp_context* ctx)
}
extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
ultramodern::GraphicsConfig graphics_config = ultramodern::get_graphics_config();
ultramodern::renderer::GraphicsConfig graphics_config = ultramodern::renderer::get_graphics_config();
float original = _arg<0, float>(rdram, ctx);
int width, height;
recomp::get_window_size(width, height);
recompui::get_window_size(width, height);
switch (graphics_config.ar_option) {
case RT64::UserConfiguration::AspectRatio::Original:
case ultramodern::renderer::AspectRatio::Original:
default:
_return(ctx, original);
return;
case RT64::UserConfiguration::AspectRatio::Expand:
case ultramodern::renderer::AspectRatio::Expand:
_return(ctx, std::max(static_cast<float>(width) / height, original));
return;
}
}
extern "C" void recomp_get_targeting_mode(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<int>(recomp::get_targeting_mode()));
_return(ctx, static_cast<int>(zelda64::get_targeting_mode()));
}
extern "C" void recomp_get_bgm_volume(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, recomp::get_bgm_volume() / 100.0f);
_return(ctx, zelda64::get_bgm_volume() / 100.0f);
}
extern "C" void recomp_get_low_health_beeps_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<u32>(recomp::get_low_health_beeps_enabled()));
_return(ctx, static_cast<u32>(zelda64::get_low_health_beeps_enabled()));
}
extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
@ -92,7 +92,7 @@ extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void recomp_autosave_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<s32>(recomp::get_autosave_mode() == recomp::AutosaveMode::On));
_return(ctx, static_cast<s32>(zelda64::get_autosave_mode() == zelda64::AutosaveMode::On));
}
extern "C" void recomp_load_overlays(uint8_t * rdram, recomp_context * ctx) {
@ -104,7 +104,7 @@ extern "C" void recomp_load_overlays(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void recomp_high_precision_fb_enabled(uint8_t * rdram, recomp_context * ctx) {
_return(ctx, static_cast<s32>(ultramodern::RT64HighPrecisionFBEnabled()));
_return(ctx, static_cast<s32>(zelda64::renderer::RT64HighPrecisionFBEnabled()));
}
extern "C" void recomp_get_resolution_scale(uint8_t* rdram, recomp_context* ctx) {
@ -115,24 +115,24 @@ extern "C" void recomp_get_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
recomp::CameraInvertMode mode = recomp::get_camera_invert_mode();
zelda64::CameraInvertMode mode = zelda64::get_camera_invert_mode();
*x_out = (mode == recomp::CameraInvertMode::InvertX || mode == recomp::CameraInvertMode::InvertBoth);
*y_out = (mode == recomp::CameraInvertMode::InvertY || mode == recomp::CameraInvertMode::InvertBoth);
*x_out = (mode == zelda64::CameraInvertMode::InvertX || mode == zelda64::CameraInvertMode::InvertBoth);
*y_out = (mode == zelda64::CameraInvertMode::InvertY || mode == zelda64::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_get_analog_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
recomp::CameraInvertMode mode = recomp::get_analog_camera_invert_mode();
zelda64::CameraInvertMode mode = zelda64::get_analog_camera_invert_mode();
*x_out = (mode == recomp::CameraInvertMode::InvertX || mode == recomp::CameraInvertMode::InvertBoth);
*y_out = (mode == recomp::CameraInvertMode::InvertY || mode == recomp::CameraInvertMode::InvertBoth);
*x_out = (mode == zelda64::CameraInvertMode::InvertX || mode == zelda64::CameraInvertMode::InvertBoth);
*y_out = (mode == zelda64::CameraInvertMode::InvertY || mode == zelda64::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_analog_cam_enabled(uint8_t* rdram, recomp_context* ctx) {
_return<s32>(ctx, recomp::get_analog_cam_mode() == recomp::AnalogCamMode::On);
_return<s32>(ctx, zelda64::get_analog_cam_mode() == zelda64::AnalogCamMode::On);
}
extern "C" void recomp_get_camera_inputs(uint8_t* rdram, recomp_context* ctx) {

View File

@ -1,9 +1,9 @@
#include <cstdint>
#include <vector>
#include <string>
#include "recomp_debug.h"
#include "zelda_debug.h"
std::vector<recomp::AreaWarps> recomp::game_warps {
std::vector<zelda64::AreaWarps> zelda64::game_warps {
{ "Clock Town", {
{
0, "Mayor's Residence", {

View File

@ -6,11 +6,12 @@
#include <filesystem>
#include <numeric>
#include <stdexcept>
#include <cinttypes>
#include "nfd.h"
#include "../../ultramodern/ultra64.h"
#include "../../ultramodern/ultramodern.hpp"
#include "ultramodern/ultra64.h"
#include "ultramodern/ultramodern.hpp"
#define SDL_MAIN_HANDLED
#ifdef _WIN32
#include "SDL.h"
@ -21,9 +22,15 @@
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_config.h"
#include "recomp_game.h"
#include "recomp_sound.h"
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#ifdef HAS_MM_SHADER_CACHE
#include "mm_shader_cache.h"
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@ -31,12 +38,8 @@
#include "SDL_syswm.h"
#endif
#define STB_IMAGE_IMPLEMENTATION
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
extern "C" void init();
/*extern "C"*/ void start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t* audio_callbacks, const ultramodern::input_callbacks_t* input_callbacks);
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
@ -115,11 +118,11 @@ bool SetImageAsIcon(const char* filename, SDL_Window* window)
SDL_Window* window;
ultramodern::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, SDL_WINDOW_RESIZABLE );
#if defined(__linux__)
SetImageAsIcon("icons/512.png",window);
if (ultramodern::get_graphics_config().wm_option == ultramodern::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
SDL_SetWindowFullscreen(window,SDL_WINDOW_FULLSCREEN_DESKTOP);
} else {
SDL_SetWindowFullscreen(window,0);
@ -135,7 +138,7 @@ ultramodern::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t
SDL_GetWindowWMInfo(window, &wmInfo);
#if defined(_WIN32)
return ultramodern::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__ANDROID__)
static_assert(false && "Unimplemented");
#elif defined(__linux__)
@ -143,7 +146,7 @@ ultramodern::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t
exit_error("Unsupported SDL2 video driver \"%s\". Only X11 is supported on Linux.\n", SDL_GetCurrentVideoDriver());
}
return ultramodern::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.window };
return ultramodern::renderer::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.window };
#else
static_assert(false && "Unimplemented");
#endif
@ -192,7 +195,7 @@ void queue_samples(int16_t* audio_data, size_t sample_count) {
// Convert the audio from 16-bit values to floats and swap the audio channels into the
// swap buffer to correct for the address xor caused by endianness handling.
float cur_main_volume = recomp::get_main_volume() / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
float cur_main_volume = zelda64::get_main_volume() / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
for (size_t i = 0; i < sample_count; i += input_channels) {
swap_buffer[i + 0 + duplicated_input_frames * input_channels] = audio_data[i + 1] * (0.5f / 32768.0f) * cur_main_volume;
swap_buffer[i + 1 + duplicated_input_frames * input_channels] = audio_data[i + 0] * (0.5f / 32768.0f) * cur_main_volume;
@ -302,6 +305,42 @@ void reset_audio(uint32_t output_freq) {
update_audio_converter();
}
extern RspUcodeFunc njpgdspMain;
extern RspUcodeFunc aspMain;
RspUcodeFunc* get_rsp_microcode(const OSTask* task) {
switch (task->t.type) {
case M_AUDTASK:
return aspMain;
case M_NJPEGTASK:
return njpgdspMain;
default:
fprintf(stderr, "Unknown task: %" PRIu32 "\n", task->t.type);
return nullptr;
}
}
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
gpr get_entrypoint_address();
// array of supported GameEntry objects
std::vector<recomp::GameEntry> supported_games = {
{
.rom_hash = 0xEF18B4A9E2386169ULL,
.internal_name = "ZELDA MAJORA'S MASK",
.game_id = u8"mm.n64.us.1.0",
#ifdef HAS_MM_SHADER_CACHE
.cache_data = {mm_shader_cache_bytes, sizeof(mm_shader_cache_bytes)},
#endif
.is_enabled = true,
.entrypoint_address = get_entrypoint_address(),
.entrypoint = recomp_entrypoint,
},
};
int main(int argc, char** argv) {
#ifdef _WIN32
@ -333,7 +372,29 @@ int main(int argc, char** argv) {
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
recomp::load_config();
// Source controller mappings file
if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) {
fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError());
}
// Register supported games and patches
for (const auto& game : supported_games) {
recomp::register_game(game);
}
zelda64::register_overlays();
zelda64::register_patches();
recomp::register_config_path(zelda64::get_app_folder_path());
zelda64::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
.get_rsp_microcode = get_rsp_microcode,
};
ultramodern::renderer::callbacks_t renderer_callbacks{
.create_render_context = zelda64::renderer::create_render_context,
};
ultramodern::gfx_callbacks_t gfx_callbacks{
.create_gfx = create_gfx,
@ -347,14 +408,24 @@ int main(int argc, char** argv) {
.set_frequency = set_frequency,
};
ultramodern::input_callbacks_t input_callbacks{
ultramodern::input::callbacks_t input_callbacks{
.poll_input = recomp::poll_inputs,
.get_input = recomp::get_n64_input,
.set_rumble = recomp::set_rumble,
.get_connected_device_info = recomp::get_connected_device_info,
};
recomp::start({}, audio_callbacks, input_callbacks, gfx_callbacks);
ultramodern::events::callbacks_t thread_callbacks{
.vi_callback = recomp::update_rumble,
.gfx_init_callback = recompui::update_supported_options,
};
ultramodern::error_handling::callbacks_t error_handling_callbacks{
.message_box = recompui::message_box,
};
recomp::start({}, rsp_callbacks, renderer_callbacks, audio_callbacks, input_callbacks, gfx_callbacks, thread_callbacks, error_handling_callbacks);
NFD_Quit();
return EXIT_SUCCESS;

View File

@ -0,0 +1,19 @@
#include "ovl_patches.hpp"
#include "../../RecompiledFuncs/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
void zelda64::register_overlays() {
recomp::overlays::overlay_section_table_data_t sections {
.code_sections = section_table,
.num_code_sections = ARRLEN(section_table),
.total_num_sections = num_sections,
};
recomp::overlays::overlays_by_index_t overlays {
.table = overlay_sections_by_index,
.len = ARRLEN(overlay_sections_by_index),
};
recomp::overlays::register_overlays(sections, overlays);
}

View File

@ -0,0 +1,10 @@
#include "ovl_patches.hpp"
#include "../../RecompiledPatches/patches_bin.h"
#include "../../RecompiledPatches/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
#include "librecomp/game.hpp"
void zelda64::register_patches() {
recomp::overlays::register_patches(mm_patches_bin, sizeof(mm_patches_bin), section_table);
}

View File

@ -1,13 +1,15 @@
#include <memory>
#include <cstring>
// #include <Windows.h>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_layer.h"
#include "rt64_render_hooks.h"
ultramodern::RT64Context::~RT64Context() = default;
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
#include "zelda_render.h"
#include "recomp_ui.h"
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
@ -42,9 +44,7 @@ unsigned int VI_V_BURST_REG = 0;
unsigned int VI_X_SCALE_REG = 0;
unsigned int VI_Y_SCALE_REG = 0;
void dummy_check_interrupts() {
}
void dummy_check_interrupts() {}
RT64::UserConfiguration::Antialiasing compute_max_supported_aa(RT64::RenderSampleCounts bits) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_2) {
@ -59,77 +59,122 @@ RT64::UserConfiguration::Antialiasing compute_max_supported_aa(RT64::RenderSampl
return RT64::UserConfiguration::Antialiasing::None;
}
RT64::UserConfiguration::InternalColorFormat to_rt64(ultramodern::HighPrecisionFramebuffer option) {
RT64::UserConfiguration::AspectRatio to_rt64(ultramodern::renderer::AspectRatio option) {
switch (option) {
case ultramodern::HighPrecisionFramebuffer::Off:
case ultramodern::renderer::AspectRatio::Original:
return RT64::UserConfiguration::AspectRatio::Original;
case ultramodern::renderer::AspectRatio::Expand:
return RT64::UserConfiguration::AspectRatio::Expand;
case ultramodern::renderer::AspectRatio::Manual:
return RT64::UserConfiguration::AspectRatio::Manual;
case ultramodern::renderer::AspectRatio::OptionCount:
return RT64::UserConfiguration::AspectRatio::OptionCount;
}
}
RT64::UserConfiguration::Antialiasing to_rt64(ultramodern::renderer::Antialiasing option) {
switch (option) {
case ultramodern::renderer::Antialiasing::None:
return RT64::UserConfiguration::Antialiasing::None;
case ultramodern::renderer::Antialiasing::MSAA2X:
return RT64::UserConfiguration::Antialiasing::MSAA2X;
case ultramodern::renderer::Antialiasing::MSAA4X:
return RT64::UserConfiguration::Antialiasing::MSAA4X;
case ultramodern::renderer::Antialiasing::MSAA8X:
return RT64::UserConfiguration::Antialiasing::MSAA8X;
case ultramodern::renderer::Antialiasing::OptionCount:
return RT64::UserConfiguration::Antialiasing::OptionCount;
}
}
RT64::UserConfiguration::RefreshRate to_rt64(ultramodern::renderer::RefreshRate option) {
switch (option) {
case ultramodern::renderer::RefreshRate::Original:
return RT64::UserConfiguration::RefreshRate::Original;
case ultramodern::renderer::RefreshRate::Display:
return RT64::UserConfiguration::RefreshRate::Display;
case ultramodern::renderer::RefreshRate::Manual:
return RT64::UserConfiguration::RefreshRate::Manual;
case ultramodern::renderer::RefreshRate::OptionCount:
return RT64::UserConfiguration::RefreshRate::OptionCount;
}
}
RT64::UserConfiguration::InternalColorFormat to_rt64(ultramodern::renderer::HighPrecisionFramebuffer option) {
switch (option) {
case ultramodern::renderer::HighPrecisionFramebuffer::Off:
return RT64::UserConfiguration::InternalColorFormat::Standard;
case ultramodern::HighPrecisionFramebuffer::On:
case ultramodern::renderer::HighPrecisionFramebuffer::On:
return RT64::UserConfiguration::InternalColorFormat::High;
case ultramodern::HighPrecisionFramebuffer::Auto:
case ultramodern::renderer::HighPrecisionFramebuffer::Auto:
return RT64::UserConfiguration::InternalColorFormat::Automatic;
default:
case ultramodern::renderer::HighPrecisionFramebuffer::OptionCount:
return RT64::UserConfiguration::InternalColorFormat::OptionCount;
}
}
void set_application_user_config(RT64::Application* application, const ultramodern::GraphicsConfig& config) {
void set_application_user_config(RT64::Application* application, const ultramodern::renderer::GraphicsConfig& config) {
switch (config.res_option) {
default:
case ultramodern::Resolution::Auto:
case ultramodern::renderer::Resolution::Auto:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::WindowIntegerScale;
application->userConfig.downsampleMultiplier = 1;
break;
case ultramodern::Resolution::Original:
case ultramodern::renderer::Resolution::Original:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = config.ds_option;
application->userConfig.downsampleMultiplier = config.ds_option;
application->userConfig.resolutionMultiplier = std::max(config.ds_option, 1);
application->userConfig.downsampleMultiplier = std::max(config.ds_option, 1);
break;
case ultramodern::Resolution::Original2x:
case ultramodern::renderer::Resolution::Original2x:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = 2.0 * config.ds_option;
application->userConfig.downsampleMultiplier = config.ds_option;
application->userConfig.resolutionMultiplier = 2.0 * std::max(config.ds_option, 1);
application->userConfig.downsampleMultiplier = std::max(config.ds_option, 1);
break;
}
switch (config.hr_option) {
default:
case ultramodern::HUDRatioMode::Original:
case ultramodern::renderer::HUDRatioMode::Original:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Original;
break;
case ultramodern::HUDRatioMode::Clamp16x9:
case ultramodern::renderer::HUDRatioMode::Clamp16x9:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Manual;
application->userConfig.extAspectTarget = 16.0/9.0;
break;
case ultramodern::HUDRatioMode::Full:
case ultramodern::renderer::HUDRatioMode::Full:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Expand;
break;
}
application->userConfig.aspectRatio = config.ar_option;
application->userConfig.antialiasing = config.msaa_option;
application->userConfig.refreshRate = config.rr_option;
application->userConfig.aspectRatio = to_rt64(config.ar_option);
application->userConfig.antialiasing = to_rt64(config.msaa_option);
application->userConfig.refreshRate = to_rt64(config.rr_option);
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
}
ultramodern::RT64SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
switch (rt64_result) {
case RT64::Application::SetupResult::Success:
return ultramodern::RT64SetupResult::Success;
return ultramodern::renderer::SetupResult::Success;
case RT64::Application::SetupResult::DynamicLibrariesNotFound:
return ultramodern::RT64SetupResult::DynamicLibrariesNotFound;
return ultramodern::renderer::SetupResult::DynamicLibrariesNotFound;
case RT64::Application::SetupResult::InvalidGraphicsAPI:
return ultramodern::RT64SetupResult::InvalidGraphicsAPI;
return ultramodern::renderer::SetupResult::InvalidGraphicsAPI;
case RT64::Application::SetupResult::GraphicsAPINotFound:
return ultramodern::RT64SetupResult::GraphicsAPINotFound;
return ultramodern::renderer::SetupResult::GraphicsAPINotFound;
case RT64::Application::SetupResult::GraphicsDeviceNotFound:
return ultramodern::RT64SetupResult::GraphicsDeviceNotFound;
return ultramodern::renderer::SetupResult::GraphicsDeviceNotFound;
}
fprintf(stderr, "Unhandled `RT64::Application::SetupResult` ?\n");
assert(false);
std::exit(EXIT_FAILURE);
}
ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle window_handle, bool debug) {
zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
set_rt64_hooks();
recompui::set_render_hooks();
// Set up the RT64 application core fields.
RT64::Application::Core appCore{};
@ -140,6 +185,9 @@ ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle
#elif defined(__linux__)
appCore.window.display = window_handle.display;
appCore.window.window = window_handle.window;
#elif defined(__APPLE__)
appCore.window.window = window_handle.window;
appCore.window.view = window_handle.view;
#endif
appCore.checkInterrupts = dummy_check_interrupts;
@ -183,7 +231,7 @@ ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle
app = std::make_unique<RT64::Application>(appCore, appConfig);
// Set initial user config settings based on the current settings.
ultramodern::GraphicsConfig cur_config = ultramodern::get_graphics_config();
auto& cur_config = ultramodern::renderer::get_graphics_config();
set_application_user_config(app.get(), cur_config);
app->userConfig.developerMode = debug;
// Force gbi depth branches to prevent LODs from kicking in.
@ -192,14 +240,14 @@ ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle
app->enhancementConfig.textureLOD.scale = true;
// Pick an API if the user has set an override.
switch (cur_config.api_option) {
case ultramodern::GraphicsApi::D3D12:
case ultramodern::renderer::GraphicsApi::D3D12:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::D3D12;
break;
case ultramodern::GraphicsApi::Vulkan:
case ultramodern::renderer::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
default:
case ultramodern::GraphicsApi::Auto:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
break;
}
@ -210,13 +258,13 @@ ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
if (setup_result != ultramodern::RT64SetupResult::Success) {
if (setup_result != ultramodern::renderer::SetupResult::Success) {
app = nullptr;
return;
}
// Set the application's fullscreen state.
app->setFullScreen(cur_config.wm_option == ultramodern::WindowMode::Fullscreen);
app->setFullScreen(cur_config.wm_option == ultramodern::renderer::WindowMode::Fullscreen);
// Check if the selected device actually supports MSAA sample positions and MSAA for for the formats that will be used
// and downgrade the configuration accordingly.
@ -235,27 +283,33 @@ ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle
high_precision_fb_enabled = app->shaderLibrary->usesHDR;
}
void ultramodern::RT64Context::send_dl(const OSTask* task) {
zelda64::renderer::RT64Context::~RT64Context() = default;
void zelda64::renderer::RT64Context::send_dl(const OSTask* task) {
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
}
void ultramodern::RT64Context::update_screen(uint32_t vi_origin) {
void zelda64::renderer::RT64Context::update_screen(uint32_t vi_origin) {
VI_ORIGIN_REG = vi_origin;
app->updateScreen();
}
void ultramodern::RT64Context::shutdown() {
void zelda64::renderer::RT64Context::shutdown() {
if (app != nullptr) {
app->end();
}
}
void ultramodern::RT64Context::update_config(const ultramodern::GraphicsConfig& old_config, const ultramodern::GraphicsConfig& new_config) {
bool zelda64::renderer::RT64Context::update_config(const ultramodern::renderer::GraphicsConfig& old_config, const ultramodern::renderer::GraphicsConfig& new_config) {
if (old_config == new_config) {
return false;
}
if (new_config.wm_option != old_config.wm_option) {
app->setFullScreen(new_config.wm_option == ultramodern::WindowMode::Fullscreen);
app->setFullScreen(new_config.wm_option == ultramodern::renderer::WindowMode::Fullscreen);
}
set_application_user_config(app.get(), new_config);
@ -265,20 +319,21 @@ void ultramodern::RT64Context::update_config(const ultramodern::GraphicsConfig&
if (new_config.msaa_option != old_config.msaa_option) {
app->updateMultisampling();
}
return true;
}
void ultramodern::RT64Context::enable_instant_present() {
void zelda64::renderer::RT64Context::enable_instant_present() {
// Enable the present early presentation mode for minimal latency.
app->enhancementConfig.presentation.mode = RT64::EnhancementConfiguration::Presentation::Mode::PresentEarly;
app->updateEnhancementConfig();
}
uint32_t ultramodern::RT64Context::get_display_framerate() {
uint32_t zelda64::renderer::RT64Context::get_display_framerate() const {
return app->presentQueue->ext.sharedResources->swapChainRate;
}
float ultramodern::RT64Context::get_resolution_scale() {
float zelda64::renderer::RT64Context::get_resolution_scale() const {
constexpr int ReferenceHeight = 240;
switch (app->userConfig.resolution) {
case RT64::UserConfiguration::Resolution::WindowIntegerScale:
@ -296,7 +351,7 @@ float ultramodern::RT64Context::get_resolution_scale() {
}
}
void ultramodern::RT64Context::load_shader_cache(std::span<const char> cache_binary) {
void zelda64::renderer::RT64Context::load_shader_cache(std::span<const char> cache_binary) {
// TODO figure out how to avoid a copy here.
std::istringstream cache_stream{std::string{cache_binary.data(), cache_binary.size()}};
@ -306,14 +361,18 @@ void ultramodern::RT64Context::load_shader_cache(std::span<const char> cache_bin
}
}
RT64::UserConfiguration::Antialiasing ultramodern::RT64MaxMSAA() {
RT64::UserConfiguration::Antialiasing zelda64::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
bool ultramodern::RT64SamplePositionsSupported() {
std::unique_ptr<ultramodern::renderer::RendererContext> zelda64::renderer::create_render_context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode) {
return std::make_unique<zelda64::renderer::RT64Context>(rdram, window_handle, developer_mode);
}
bool zelda64::renderer::RT64SamplePositionsSupported() {
return sample_positions_supported;
}
bool ultramodern::RT64HighPrecisionFBEnabled() {
bool zelda64::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}

View File

@ -1,29 +0,0 @@
#include "recomp.h"
#include <cstdio>
#include <string>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#define VI_NTSC_CLOCK 48681812
extern "C" void osAiSetFrequency_recomp(uint8_t* rdram, recomp_context* ctx) {
uint32_t freq = ctx->r4;
// This makes actual audio frequency more accurate to console, but may not be desirable
//uint32_t dacRate = (uint32_t)(((float)VI_NTSC_CLOCK / freq) + 0.5f);
//freq = VI_NTSC_CLOCK / dacRate;
ctx->r2 = freq;
ultramodern::set_audio_frequency(freq);
}
extern "C" void osAiSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ultramodern::queue_audio_buffer(rdram, ctx->r4, ctx->r5);
ctx->r2 = 0;
}
extern "C" void osAiGetLength_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = ultramodern::get_remaining_audio_bytes();
}
extern "C" void osAiGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = 0x00000000; // Pretend the audio DMAs finish instantly
}

View File

@ -1,155 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp_helpers.h"
static ultramodern::input_callbacks_t input_callbacks;
std::chrono::high_resolution_clock::time_point input_poll_time;
void update_poll_time() {
input_poll_time = std::chrono::high_resolution_clock::now();
}
extern "C" void recomp_set_current_frame_poll_id(uint8_t* rdram, recomp_context* ctx) {
// TODO reimplement the system for tagging polls with IDs to handle games with multithreaded input polling.
}
extern "C" void recomp_measure_latency(uint8_t* rdram, recomp_context* ctx) {
ultramodern::measure_input_latency();
}
void ultramodern::measure_input_latency() {
// printf("Delta: %ld micros\n", std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - input_poll_time));
}
void set_input_callbacks(const ultramodern::input_callbacks_t& callbacks) {
input_callbacks = callbacks;
}
static int max_controllers = 0;
extern "C" void osContInit_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) bitpattern = _arg<1, PTR(void)>(rdram, ctx);
PTR(void) status = _arg<2, PTR(void)>(rdram, ctx);
// Set bit 0 to indicate that controller 0 is present
MEM_B(0, bitpattern) = 0x01;
// Mark controller 0 as present
MEM_H(0, status) = 0x0005; // type: CONT_TYPE_NORMAL (from joybus)
MEM_B(2, status) = 0x00; // status: 0 (from joybus)
MEM_B(3, status) = 0x00; // errno: 0 (from libultra)
max_controllers = 4;
// Mark controllers 1-3 as not connected
for (size_t controller = 1; controller < max_controllers; controller++) {
// Libultra doesn't write status or type for absent controllers
MEM_B(4 * controller + 3, status) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
_return<s32>(ctx, 0);
}
extern "C" void osContStartReadData_recomp(uint8_t* rdram, recomp_context* ctx) {
if (input_callbacks.poll_input) {
input_callbacks.poll_input();
}
update_poll_time();
ultramodern::send_si_message(rdram);
}
extern "C" void osContGetReadData_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pad = _arg<0, PTR(void)>(rdram, ctx);
uint16_t buttons = 0;
float x = 0.0f;
float y = 0.0f;
if (input_callbacks.get_input) {
input_callbacks.get_input(&buttons, &x, &y);
}
if (max_controllers > 0) {
// button
MEM_H(0, pad) = buttons;
// stick_x
MEM_B(2, pad) = (int8_t)(127 * x);
// stick_y
MEM_B(3, pad) = (int8_t)(127 * y);
// errno
MEM_B(4, pad) = 0;
}
for (int controller = 1; controller < max_controllers; controller++) {
MEM_B(6 * controller + 4, pad) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
}
extern "C" void osContStartQuery_recomp(uint8_t * rdram, recomp_context * ctx) {
ultramodern::send_si_message(rdram);
}
extern "C" void osContGetQuery_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(void) status = _arg<0, PTR(void)>(rdram, ctx);
// Mark controller 0 as present
MEM_H(0, status) = 0x0005; // type: CONT_TYPE_NORMAL (from joybus)
MEM_B(2, status) = 0x01; // status: 0x01 (from joybus, indicates that a pak is plugged into the controller)
MEM_B(3, status) = 0x00; // errno: 0 (from libultra)
// Mark controllers 1-3 as not connected
for (size_t controller = 1; controller < max_controllers; controller++) {
// Libultra doesn't write status or type for absent controllers
MEM_B(4 * controller + 3, status) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
}
extern "C" void osContSetCh_recomp(uint8_t* rdram, recomp_context* ctx) {
max_controllers = std::min(_arg<0, u8>(rdram, ctx), u8(4));
_return<s32>(ctx, 0);
}
extern "C" void __osMotorAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 flag = _arg<1, s32>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(flag);
}
_return<s32>(ctx, 0);
}
extern "C" void osMotorInit_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<1, PTR(void)>(rdram, ctx);
s32 channel = _arg<2, s32>(rdram, ctx);
MEM_W(8, pfs) = channel;
_return<s32>(ctx, 0);
}
extern "C" void osMotorStart_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(true);
}
_return<s32>(ctx, 0);
}
extern "C" void osMotorStop_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(false);
}
_return<s32>(ctx, 0);
}

View File

@ -1,44 +0,0 @@
#include "recomp.h"
enum class RDPStatusBit {
XbusDmem = 0,
Freeze = 1,
Flush = 2,
CommandBusy = 6,
BufferReady = 7,
DmaBusy = 8,
EndValid = 9,
StartValid = 10,
};
constexpr void update_bit(uint32_t& state, uint32_t flags, RDPStatusBit bit) {
int set_bit_pos = (int)bit * 2 + 0;
int reset_bit_pos = (int)bit * 2 + 1;
bool set = (flags & (1U << set_bit_pos)) != 0;
bool reset = (flags & (1U << reset_bit_pos)) != 0;
if (set ^ reset) {
if (set) {
state |= (1U << (int)bit);
}
else {
state &= ~(1U << (int)bit);
}
}
}
uint32_t rdp_state = 1 << (int)RDPStatusBit::BufferReady;
extern "C" void osDpSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);
}
extern "C" void osDpGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = rdp_state;
}
extern "C" void osDpSetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
update_bit(rdp_state, ctx->r4, RDPStatusBit::XbusDmem);
update_bit(rdp_state, ctx->r4, RDPStatusBit::Freeze);
update_bit(rdp_state, ctx->r4, RDPStatusBit::Flush);
}

View File

@ -1,49 +0,0 @@
#include "recomp.h"
#include "../ultramodern/ultra64.h"
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
constexpr int eeprom_block_size = 8;
constexpr int eep4_size = 4096;
constexpr int eep4_block_count = eep4_size / eeprom_block_size;
constexpr int eep16_size = 16384;
constexpr int eep16_block_count = eep16_size / eeprom_block_size;
extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = 0x02; // EEP16K
}
extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);// ctx->r2 = 8; // CONT_NO_RESPONSE_ERROR
}
extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) {
uint8_t eep_address = ctx->r5;
gpr buffer = ctx->r6;
int32_t nbytes = ctx->r7;
assert(!(nbytes & 7));
assert(eep_address * eeprom_block_size + nbytes <= eep16_size);
save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes);
ctx->r2 = 0;
}
extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);// ctx->r2 = 8; // CONT_NO_RESPONSE_ERROR
}
extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) {
uint8_t eep_address = ctx->r5;
gpr buffer = ctx->r6;
int32_t nbytes = ctx->r7;
assert(!(nbytes & 7));
assert(eep_address * eeprom_block_size + nbytes <= eep16_size);
save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes);
ctx->r2 = 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
#ifndef __EUC_JP_H__
#define __EUC_JP_H__
#include <string>
#include <string_view>
namespace Encoding {
std::string decode_eucjp(std::string_view src);
}
#endif

View File

@ -1,141 +0,0 @@
#include <array>
#include <cassert>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
// TODO move this out into ultramodern code
constexpr uint32_t flash_size = 1024 * 1024 / 8; // 1Mbit
constexpr uint32_t page_size = 128;
constexpr uint32_t pages_per_sector = 128;
constexpr uint32_t page_count = flash_size / page_size;
constexpr uint32_t sector_size = page_size * pages_per_sector;
constexpr uint32_t sector_count = flash_size / sector_size;
void save_write_ptr(const void* in, uint32_t offset, uint32_t count);
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_clear(uint32_t start, uint32_t size, char value);
std::array<char, page_size> write_buffer;
extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = ultramodern::flash_handle;
}
extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(u8) flash_status = ctx->r4;
MEM_B(0, flash_status) = 0;
}
extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(u32) flash_type = ctx->r4;
PTR(u32) flash_maker = ctx->r5;
// Mimic a real flash chip's type and maker, as some games actually check if one is present.
MEM_W(0, flash_type) = 0x11118001;
MEM_W(0, flash_maker) = 0x00C2001E;
}
extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void osFlashAllErase_recomp(uint8_t * rdram, recomp_context * ctx) {
save_clear(0, ultramodern::save_size, 0xFF);
ctx->r2 = 0;
}
extern "C" void osFlashAllEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) {
save_clear(0, ultramodern::save_size, 0xFF);
ctx->r2 = 0;
}
// This function is named sector but really means page.
extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = (uint32_t)ctx->r4;
// Prevent out of bounds erase
if (page_num >= page_count) {
ctx->r2 = -1;
return;
}
save_clear(page_num * page_size, page_size, 0xFF);
ctx->r2 = 0;
}
// Same naming issue as above.
extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = (uint32_t)ctx->r4;
// Prevent out of bounds erase
if (page_num >= page_count) {
ctx->r2 = -1;
return;
}
save_clear(page_num * page_size, page_size, 0xFF);
ctx->r2 = 0;
}
extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ctx) {
// All erases are blocking in this implementation, so this should always return OK.
ctx->r2 = 0; // FLASH_STATUS_ERASE_OK
}
extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) {
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4);
int32_t pri = ctx->r5;
PTR(void) dramAddr = ctx->r6;
PTR(OSMesgQueue) mq = ctx->r7;
// Copy the input data into the write buffer
for (size_t i = 0; i < page_size; i++) {
write_buffer[i] = MEM_B(i, dramAddr);
}
// Send the message indicating write completion
osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK);
ctx->r2 = 0;
}
extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = ctx->r4;
// Copy the write buffer into the save file
save_write_ptr(write_buffer.data(), page_num * page_size, page_size);
ctx->r2 = 0;
}
extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) {
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4);
int32_t pri = ctx->r5;
uint32_t page_num = ctx->r6;
PTR(void) dramAddr = ctx->r7;
uint32_t n_pages = MEM_W(0x10, ctx->r29);
PTR(OSMesgQueue) mq = MEM_W(0x14, ctx->r29);
uint32_t offset = page_num * page_size;
uint32_t count = n_pages * page_size;
// Read from the save file into the provided buffer
save_read(PASS_RDRAM dramAddr, offset, count);
// Send the message indicating read completion
osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK);
ctx->r2 = 0;
}
extern "C" void osFlashChange_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}

View File

@ -1,80 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void __udivdi3_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __divdi3_recomp(uint8_t * rdram, recomp_context * ctx) {
int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
int64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __umoddi3_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a % b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_div_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ll_div_recomp(uint8_t * rdram, recomp_context * ctx) {
int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
int64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ll_mul_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a * b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_rem_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a % b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_to_d_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
double ret = (double)a;
ctx->f0.d = ret;
}
extern "C" void __ull_to_f_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
float ret = (float)a;
ctx->f0.fl = ret;
}

View File

@ -1,165 +0,0 @@
#include <unordered_map>
#include <algorithm>
#include <vector>
#include "recomp.h"
#include "recomp_overlays.h"
#include "../RecompiledFuncs/recomp_overlays.inl"
constexpr size_t num_code_sections = ARRLEN(section_table);
// SectionTableEntry sections[] defined in recomp_overlays.inl
struct LoadedSection {
int32_t loaded_ram_addr;
size_t section_table_index;
LoadedSection(int32_t loaded_ram_addr_, size_t section_table_index_) {
loaded_ram_addr = loaded_ram_addr_;
section_table_index = section_table_index_;
}
bool operator<(const LoadedSection& rhs) {
return loaded_ram_addr < rhs.loaded_ram_addr;
}
};
std::vector<LoadedSection> loaded_sections{};
std::unordered_map<int32_t, recomp_func_t*> func_map{};
void load_overlay(size_t section_table_index, int32_t ram) {
const SectionTableEntry& section = section_table[section_table_index];
for (size_t function_index = 0; function_index < section.num_funcs; function_index++) {
const FuncEntry& func = section.funcs[function_index];
func_map[ram + func.offset] = func.func;
}
loaded_sections.emplace_back(ram, section_table_index);
section_addresses[section.index] = ram;
}
void load_special_overlay(const SectionTableEntry& section, int32_t ram) {
for (size_t function_index = 0; function_index < section.num_funcs; function_index++) {
const FuncEntry& func = section.funcs[function_index];
func_map[ram + func.offset] = func.func;
}
}
extern "C" {
int32_t section_addresses[num_sections];
}
extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size) {
// Search for the first section that's included in the loaded rom range
// Sections were sorted by `init_overlays` so we can use the bounds functions
auto lower = std::lower_bound(&section_table[0], &section_table[num_code_sections], rom,
[](const SectionTableEntry& entry, uint32_t addr) {
return entry.rom_addr < addr;
}
);
auto upper = std::upper_bound(&section_table[0], &section_table[num_code_sections], (uint32_t)(rom + size),
[](uint32_t addr, const SectionTableEntry& entry) {
return addr < entry.size + entry.rom_addr;
}
);
// Load the overlays that were found
for (auto it = lower; it != upper; ++it) {
load_overlay(std::distance(&section_table[0], it), it->rom_addr - rom + ram_addr);
}
}
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size);
extern "C" void unload_overlay_by_id(uint32_t id) {
uint32_t section_table_index = overlay_sections_by_index[id];
const SectionTableEntry& section = section_table[section_table_index];
auto find_it = std::find_if(loaded_sections.begin(), loaded_sections.end(), [section_table_index](const LoadedSection& s) { return s.section_table_index == section_table_index; });
if (find_it != loaded_sections.end()) {
// Determine where each function was loaded to and remove that entry from the function map
for (size_t func_index = 0; func_index < section.num_funcs; func_index++) {
const auto& func = section.funcs[func_index];
uint32_t func_address = func.offset + find_it->loaded_ram_addr;
func_map.erase(func_address);
}
// Reset the section's address in the address table
section_addresses[section.index] = section.ram_addr;
// Remove the section from the loaded section map
loaded_sections.erase(find_it);
}
}
extern "C" void load_overlay_by_id(uint32_t id, uint32_t ram_addr) {
uint32_t section_table_index = overlay_sections_by_index[id];
const SectionTableEntry& section = section_table[section_table_index];
int32_t prev_address = section_addresses[section.index];
if (/*ram_addr >= 0x80000000 && ram_addr < 0x81000000) {*/ prev_address == section.ram_addr) {
load_overlay(section_table_index, ram_addr);
}
else {
int32_t new_address = prev_address + ram_addr;
unload_overlay_by_id(id);
load_overlay(section_table_index, new_address);
}
}
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size) {
for (auto it = loaded_sections.begin(); it != loaded_sections.end();) {
const auto& section = section_table[it->section_table_index];
// Check if the unloaded region overlaps with the loaded section
if (ram_addr < (it->loaded_ram_addr + section.size) && (ram_addr + size) >= it->loaded_ram_addr) {
// Check if the section isn't entirely in the loaded region
if (ram_addr > it->loaded_ram_addr || (ram_addr + size) < (it->loaded_ram_addr + section.size)) {
fprintf(stderr,
"Cannot partially unload section\n"
" rom: 0x%08X size: 0x%08X loaded_addr: 0x%08X\n"
" unloaded_ram: 0x%08X unloaded_size : 0x%08X\n",
section.rom_addr, section.size, it->loaded_ram_addr, ram_addr, size);
assert(false);
std::exit(EXIT_FAILURE);
}
// Determine where each function was loaded to and remove that entry from the function map
for (size_t func_index = 0; func_index < section.num_funcs; func_index++) {
const auto& func = section.funcs[func_index];
uint32_t func_address = func.offset + it->loaded_ram_addr;
func_map.erase(func_address);
}
// Reset the section's address in the address table
section_addresses[section.index] = section.ram_addr;
// Remove the section from the loaded section map
it = loaded_sections.erase(it);
// Skip incrementing the iterator
continue;
}
++it;
}
}
void load_patch_functions();
void init_overlays() {
for (size_t section_index = 0; section_index < num_code_sections; section_index++) {
section_addresses[section_table[section_index].index] = section_table[section_index].ram_addr;
}
// Sort the executable sections by rom address
std::sort(&section_table[0], &section_table[num_code_sections],
[](const SectionTableEntry& a, const SectionTableEntry& b) {
return a.rom_addr < b.rom_addr;
}
);
load_patch_functions();
}
extern "C" recomp_func_t * get_function(int32_t addr) {
auto func_find = func_map.find(addr);
if (func_find == func_map.end()) {
fprintf(stderr, "Failed to find function at 0x%08X\n", addr);
assert(false);
std::exit(EXIT_FAILURE);
}
return func_find->second;
}

View File

@ -1,35 +0,0 @@
#include "recomp.h"
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
extern "C" void osPfsInitPak_recomp(uint8_t * rdram, recomp_context* ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFreeBlocks_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsAllocateFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsDeleteFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFileState_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFindFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsReadWriteFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsChecker_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}

View File

@ -1,11 +0,0 @@
#include <unordered_map>
#include <algorithm>
#include <vector>
#include "recomp.h"
#include "../../RecompiledPatches/recomp_overlays.inl"
void load_special_overlay(const SectionTableEntry& section, int32_t ram);
void load_patch_functions() {
load_special_overlay(section_table[0], section_table[0].ram_addr);
}

View File

@ -1,321 +0,0 @@
#include <memory>
#include <fstream>
#include <array>
#include <cstring>
#include <string>
#include <mutex>
#include "recomp.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
static std::vector<uint8_t> rom;
bool recomp::is_rom_loaded() {
return !rom.empty();
}
void recomp::set_rom_contents(std::vector<uint8_t>&& new_rom) {
rom = std::move(new_rom);
}
// Flashram occupies the same physical address as sram, but that issue is avoided because libultra exposes
// a high-level interface for flashram. Because that high-level interface is reimplemented, low level accesses
// that involve physical addresses don't need to be handled for flashram.
constexpr uint32_t sram_base = 0x08000000;
constexpr uint32_t rom_base = 0x10000000;
constexpr uint32_t drive_base = 0x06000000;
constexpr uint32_t k1_to_phys(uint32_t addr) {
return addr & 0x1FFFFFFF;
}
constexpr uint32_t phys_to_k1(uint32_t addr) {
return addr | 0xA0000000;
}
extern "C" void __osPiGetAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void __osPiRelAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void osCartRomInit_recomp(uint8_t* rdram, recomp_context* ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ultramodern::cart_handle);
handle->type = 0; // cart
handle->baseAddress = phys_to_k1(rom_base);
handle->domain = 0;
ctx->r2 = (gpr)ultramodern::cart_handle;
}
extern "C" void osDriveRomInit_recomp(uint8_t * rdram, recomp_context * ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ultramodern::drive_handle);
handle->type = 1; // bulk
handle->baseAddress = phys_to_k1(drive_base);
handle->domain = 0;
ctx->r2 = (gpr)ultramodern::drive_handle;
}
extern "C" void osCreatePiManager_recomp(uint8_t* rdram, recomp_context* ctx) {
;
}
void recomp::do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes) {
// TODO use word copies when possible
// TODO handle misaligned DMA
assert((physical_addr & 0x1) == 0 && "Only PI DMA from aligned ROM addresses is currently supported");
assert((ram_address & 0x7) == 0 && "Only PI DMA to aligned RDRAM addresses is currently supported");
assert((num_bytes & 0x1) == 0 && "Only PI DMA with aligned sizes is currently supported");
uint8_t* rom_addr = rom.data() + physical_addr - rom_base;
for (size_t i = 0; i < num_bytes; i++) {
MEM_B(i, ram_address) = *rom_addr;
rom_addr++;
}
}
void recomp::do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr) {
assert((physical_addr & 0x3) == 0 && "PIO not 4-byte aligned in device, currently unsupported");
assert((ram_address & 0x3) == 0 && "PIO not 4-byte aligned in RDRAM, currently unsupported");
uint8_t* rom_addr = rom.data() + physical_addr - rom_base;
MEM_B(0, ram_address) = *rom_addr++;
MEM_B(1, ram_address) = *rom_addr++;
MEM_B(2, ram_address) = *rom_addr++;
MEM_B(3, ram_address) = *rom_addr++;
}
struct {
std::array<char, 0x20000> save_buffer;
std::thread saving_thread;
moodycamel::LightweightSemaphore write_sempahore;
std::mutex save_buffer_mutex;
} save_context;
const std::u8string save_folder = u8"saves";
const std::u8string save_filename = std::u8string{recomp::mm_game_id} + u8".bin";
const std::u8string save_filename_temp = std::u8string{recomp::mm_game_id} + u8".bin.temp";
const std::u8string save_filename_backup = std::u8string{recomp::mm_game_id} + u8".bin.bak";
std::filesystem::path get_save_file_path() {
return recomp::get_app_folder_path() / save_folder / save_filename;
}
std::filesystem::path get_save_file_path_temp() {
return recomp::get_app_folder_path() / save_folder / save_filename_temp;
}
std::filesystem::path get_save_file_path_backup() {
return recomp::get_app_folder_path() / save_folder / save_filename_backup;
}
void update_save_file() {
{
std::ofstream save_file{ get_save_file_path_temp(), std::ios_base::binary };
if (save_file.good()) {
std::lock_guard lock{ save_context.save_buffer_mutex };
save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size());
}
else {
recomp::message_box("Failed to write to the save file. Check your file permissions. If you have moved your appdata folder to Dropbox or similar, this can cause issues.");
}
}
std::error_code ec;
if (std::filesystem::exists(get_save_file_path(), ec)) {
std::filesystem::copy_file(get_save_file_path(), get_save_file_path_backup(), std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
printf("[ERROR] Failed to copy save file backup\n");
}
}
std::filesystem::copy_file(get_save_file_path_temp(), get_save_file_path(), std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
recomp::message_box("Failed to write to the save file. Check your file permissions. If you have moved your appdata folder to Dropbox or similar, this can cause issues.");
}
}
extern std::atomic_bool exited;
void saving_thread_func(RDRAM_ARG1) {
while (!exited) {
bool save_buffer_updated = false;
// Repeatedly wait for a new action to be sent.
constexpr int64_t wait_time_microseconds = 10000;
constexpr int max_actions = 128;
int num_actions = 0;
// Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save.
// Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game
// is constantly sending writes.
while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) {
save_buffer_updated = true;
num_actions++;
}
// If an action came through that affected the save file, save the updated contents.
if (save_buffer_updated) {
update_save_file();
}
}
}
void save_write_ptr(const void* in, uint32_t offset, uint32_t count) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
memcpy(&save_context.save_buffer[offset], in, count);
}
save_context.write_sempahore.signal();
}
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
for (uint32_t i = 0; i < count; i++) {
save_context.save_buffer[offset + i] = MEM_B(i, rdram_address);
}
}
save_context.write_sempahore.signal();
}
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) {
std::lock_guard lock { save_context.save_buffer_mutex };
for (size_t i = 0; i < count; i++) {
MEM_B(i, rdram_address) = save_context.save_buffer[offset + i];
}
}
void save_clear(uint32_t start, uint32_t size, char value) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
std::fill_n(save_context.save_buffer.begin() + start, size, value);
}
save_context.write_sempahore.signal();
}
void ultramodern::init_saving(RDRAM_ARG1) {
std::filesystem::path save_file_path = get_save_file_path();
std::filesystem::path save_file_path_backup = get_save_file_path_backup();
// Ensure the save file directory exists.
std::filesystem::create_directories(save_file_path.parent_path());
// Read the save file if it exists.
std::ifstream save_file{ save_file_path, std::ios_base::binary };
if (save_file.good()) {
save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size());
} else {
// Reading the save file faield, so try to read the backup save file.
std::ifstream save_file_backup{ save_file_path_backup, std::ios_base::binary };
if (save_file_backup.good()) {
save_file_backup.read(save_context.save_buffer.data(), save_context.save_buffer.size());
} else {
// Otherwise clear the save file to all zeroes.
save_context.save_buffer.fill(0);
}
}
save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM};
}
void ultramodern::join_saving_thread() {
save_context.saving_thread.join();
}
void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_addr, uint32_t size, uint32_t direction) {
// TODO asynchronous transfer
// TODO implement unaligned DMA correctly
if (direction == 0) {
if (physical_addr >= rom_base) {
// read cart rom
recomp::do_rom_read(rdram, rdram_address, physical_addr, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else if (physical_addr >= sram_base) {
// read sram
save_read(rdram, rdram_address, physical_addr - sram_base, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else {
fprintf(stderr, "[WARN] PI DMA read from unknown region, phys address 0x%08X\n", physical_addr);
}
} else {
if (physical_addr >= rom_base) {
// write cart rom
throw std::runtime_error("ROM DMA write unimplemented");
} else if (physical_addr >= sram_base) {
// write sram
save_write(rdram, rdram_address, physical_addr - sram_base, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else {
fprintf(stderr, "[WARN] PI DMA write to unknown region, phys address 0x%08X\n", physical_addr);
}
}
}
extern "C" void osPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) {
uint32_t mb = ctx->r4;
uint32_t pri = ctx->r5;
uint32_t direction = ctx->r6;
uint32_t devAddr = ctx->r7 | rom_base;
gpr dramAddr = MEM_W(0x10, ctx->r29);
uint32_t size = MEM_W(0x14, ctx->r29);
PTR(OSMesgQueue) mq = MEM_W(0x18, ctx->r29);
uint32_t physical_addr = k1_to_phys(devAddr);
debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size);
do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction);
ctx->r2 = 0;
}
extern "C" void osEPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4);
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r5);
uint32_t direction = ctx->r6;
uint32_t devAddr = handle->baseAddress | mb->devAddr;
gpr dramAddr = mb->dramAddr;
uint32_t size = mb->size;
PTR(OSMesgQueue) mq = mb->hdr.retQueue;
uint32_t physical_addr = k1_to_phys(devAddr);
debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size);
do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction);
ctx->r2 = 0;
}
extern "C" void osEPiReadIo_recomp(RDRAM_ARG recomp_context * ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4);
uint32_t devAddr = handle->baseAddress | ctx->r5;
gpr dramAddr = ctx->r6;
uint32_t physical_addr = k1_to_phys(devAddr);
if (physical_addr > rom_base) {
// cart rom
recomp::do_rom_pio(PASS_RDRAM dramAddr, physical_addr);
} else {
// sram
assert(false && "SRAM ReadIo unimplemented");
}
ctx->r2 = 0;
}
extern "C" void osPiGetStatus_recomp(RDRAM_ARG recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void osPiRawStartDma_recomp(RDRAM_ARG recomp_context * ctx) {
ctx->r2 = 0;
}

View File

@ -1,72 +0,0 @@
#include <vector>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
#include "euc-jp.h"
extern "C" void __checkHardware_msp_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __checkHardware_kmc_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __checkHardware_isv_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __osInitialize_msp_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osInitialize_kmc_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osInitialize_isv_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void isPrintfInit_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osRdbSend_recomp(uint8_t * rdram, recomp_context * ctx) {
gpr buf = ctx->r4;
size_t size = ctx->r5;
u32 type = (u32)ctx->r6;
std::unique_ptr<char[]> to_print = std::make_unique<char[]>(size + 1);
for (size_t i = 0; i < size; i++) {
to_print[i] = MEM_B(i, buf);
}
to_print[size] = '\x00';
fwrite(to_print.get(), 1, size, stdout);
ctx->r2 = size;
}
extern "C" void is_proutSyncPrintf_recomp(uint8_t * rdram, recomp_context * ctx) {
// Buffering to speed up print performance
static std::vector<char> print_buffer;
gpr buf = ctx->r5;
size_t size = ctx->r6;
//for (size_t i = 0; i < size; i++) {
// // Add the new character to the buffer
// char cur_char = MEM_B(i, buf);
// // If the new character is a newline, flush the buffer
// if (cur_char == '\n') {
// std::string utf8_str = Encoding::decode_eucjp(std::string_view{ print_buffer.data(), print_buffer.size() });
// puts(utf8_str.c_str());
// print_buffer.clear();
// } else {
// print_buffer.push_back(cur_char);
// }
//}
//fwrite(to_print.get(), size, 1, stdout);
ctx->r2 = 1;
}

View File

@ -1,445 +0,0 @@
#ifdef _WIN32
#include <Windows.h>
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
#include <fstream>
#include <iostream>
#include "recomp.h"
#include "recomp_overlays.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "xxHash/xxh3.h"
#include "../ultramodern/ultramodern.hpp"
#include "../../RecompiledPatches/patches_bin.h"
#ifdef HAS_MM_SHADER_CACHE
#include "mm_shader_cache.h"
#endif
#ifdef _MSC_VER
inline uint32_t byteswap(uint32_t val) {
return _byteswap_ulong(val);
}
#else
constexpr uint32_t byteswap(uint32_t val) {
return __builtin_bswap32(val);
}
#endif
struct RomEntry {
uint64_t xxhash3_value;
std::u8string stored_filename;
std::string internal_rom_name;
};
const std::unordered_map<recomp::Game, RomEntry> game_roms {
{ recomp::Game::MM, { 0xEF18B4A9E2386169ULL, std::u8string{recomp::mm_game_id} + u8".z64", "ZELDA MAJORA'S MASK" }},
};
bool check_hash(const std::vector<uint8_t>& rom_data, uint64_t expected_hash) {
uint64_t calculated_hash = XXH3_64bits(rom_data.data(), rom_data.size());
return calculated_hash == expected_hash;
}
static std::vector<uint8_t> read_file(const std::filesystem::path& path) {
std::vector<uint8_t> ret;
std::ifstream file{ path, std::ios::binary};
if (file.good()) {
file.seekg(0, std::ios::end);
ret.resize(file.tellg());
file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char*>(ret.data()), ret.size());
}
return ret;
}
bool write_file(const std::filesystem::path& path, const std::vector<uint8_t>& data) {
std::ofstream out_file{ path, std::ios::binary };
if (!out_file.good()) {
return false;
}
out_file.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
bool check_stored_rom(const RomEntry& game_entry) {
std::vector stored_rom_data = read_file(recomp::get_app_folder_path() / game_entry.stored_filename);
if (!check_hash(stored_rom_data, game_entry.xxhash3_value)) {
// Incorrect hash, remove the stored ROM file if it exists.
std::filesystem::remove(recomp::get_app_folder_path() / game_entry.stored_filename);
return false;
}
return true;
}
static std::unordered_set<recomp::Game> valid_game_roms;
bool recomp::is_rom_valid(recomp::Game game) {
return valid_game_roms.contains(game);
}
void recomp::check_all_stored_roms() {
for (const auto& cur_rom_entry: game_roms) {
if (check_stored_rom(cur_rom_entry.second)) {
valid_game_roms.insert(cur_rom_entry.first);
}
}
}
bool recomp::load_stored_rom(recomp::Game game) {
auto find_it = game_roms.find(game);
if (find_it == game_roms.end()) {
return false;
}
std::vector<uint8_t> stored_rom_data = read_file(recomp::get_app_folder_path() / find_it->second.stored_filename);
if (!check_hash(stored_rom_data, find_it->second.xxhash3_value)) {
// The ROM no longer has the right hash, delete it.
std::filesystem::remove(recomp::get_app_folder_path() / find_it->second.stored_filename);
return false;
}
recomp::set_rom_contents(std::move(stored_rom_data));
return true;
}
const std::array<uint8_t, 4> first_rom_bytes { 0x80, 0x37, 0x12, 0x40 };
enum class ByteswapType {
NotByteswapped,
Byteswapped4,
Byteswapped2,
Invalid
};
ByteswapType check_rom_start(const std::vector<uint8_t>& rom_data) {
if (rom_data.size() < 4) {
return ByteswapType::Invalid;
}
bool matched_all = true;
auto check_match = [&](uint8_t index0, uint8_t index1, uint8_t index2, uint8_t index3) {
return
rom_data[0] == first_rom_bytes[index0] &&
rom_data[1] == first_rom_bytes[index1] &&
rom_data[2] == first_rom_bytes[index2] &&
rom_data[3] == first_rom_bytes[index3];
};
// Check if the ROM is already in the correct byte order.
if (check_match(0,1,2,3)) {
return ByteswapType::NotByteswapped;
}
// Check if the ROM has been byteswapped in groups of 4 bytes.
if (check_match(3,2,1,0)) {
return ByteswapType::Byteswapped4;
}
// Check if the ROM has been byteswapped in groups of 2 bytes.
if (check_match(1,0,3,2)) {
return ByteswapType::Byteswapped2;
}
// No match found.
return ByteswapType::Invalid;
}
void byteswap_data(std::vector<uint8_t>& rom_data, size_t index_xor) {
for (size_t rom_pos = 0; rom_pos < rom_data.size(); rom_pos += 4) {
uint8_t temp0 = rom_data[rom_pos + 0];
uint8_t temp1 = rom_data[rom_pos + 1];
uint8_t temp2 = rom_data[rom_pos + 2];
uint8_t temp3 = rom_data[rom_pos + 3];
rom_data[rom_pos + (0 ^ index_xor)] = temp0;
rom_data[rom_pos + (1 ^ index_xor)] = temp1;
rom_data[rom_pos + (2 ^ index_xor)] = temp2;
rom_data[rom_pos + (3 ^ index_xor)] = temp3;
}
}
recomp::RomValidationError recomp::select_rom(const std::filesystem::path& rom_path, Game game) {
auto find_it = game_roms.find(game);
if (find_it == game_roms.end()) {
return recomp::RomValidationError::OtherError;
}
const RomEntry& game_entry = find_it->second;
std::vector<uint8_t> rom_data = read_file(rom_path);
if (rom_data.empty()) {
return recomp::RomValidationError::FailedToOpen;
}
// Pad the rom to the nearest multiple of 4 bytes.
rom_data.resize((rom_data.size() + 3) & ~3);
ByteswapType byteswap_type = check_rom_start(rom_data);
switch (byteswap_type) {
case ByteswapType::Invalid:
return recomp::RomValidationError::NotARom;
case ByteswapType::Byteswapped2:
byteswap_data(rom_data, 1);
break;
case ByteswapType::Byteswapped4:
byteswap_data(rom_data, 3);
break;
case ByteswapType::NotByteswapped:
break;
}
if (!check_hash(rom_data, game_entry.xxhash3_value)) {
const std::string_view name{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, game_entry.internal_rom_name.size()};
if (name == game_entry.internal_rom_name) {
return recomp::RomValidationError::IncorrectVersion;
}
else {
if (game == recomp::Game::MM && std::string_view{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, 19 } == "THE LEGEND OF ZELDA") {
return recomp::RomValidationError::NotYet;
}
else {
return recomp::RomValidationError::IncorrectRom;
}
}
}
write_file(recomp::get_app_folder_path() / game_entry.stored_filename, rom_data);
return recomp::RomValidationError::Good;
}
extern "C" void osGetMemSize_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 8 * 1024 * 1024;
}
enum class StatusReg {
FR = 0x04000000,
};
extern "C" void cop0_status_write(recomp_context* ctx, gpr value) {
uint32_t old_sr = ctx->status_reg;
uint32_t new_sr = (uint32_t)value;
uint32_t changed = old_sr ^ new_sr;
// Check if the FR bit changed
if (changed & (uint32_t)StatusReg::FR) {
// Check if the FR bit was set
if (new_sr & (uint32_t)StatusReg::FR) {
// FR = 1, odd single floats point to their own registers
ctx->f_odd = &ctx->f1.u32l;
ctx->mips3_float_mode = true;
}
// Otherwise, it was cleared
else {
// FR = 0, odd single floats point to the upper half of the previous register
ctx->f_odd = &ctx->f0.u32h;
ctx->mips3_float_mode = false;
}
// Remove the FR bit from the changed bits as it's been handled
changed &= ~(uint32_t)StatusReg::FR;
}
// If any other bits were changed, assert false as they're not handled currently
if (changed) {
printf("Unhandled status register bits changed: 0x%08X\n", changed);
assert(false);
exit(EXIT_FAILURE);
}
// Update the status register in the context
ctx->status_reg = new_sr;
}
extern "C" gpr cop0_status_read(recomp_context* ctx) {
return (gpr)(int32_t)ctx->status_reg;
}
extern "C" void switch_error(const char* func, uint32_t vram, uint32_t jtbl) {
printf("Switch-case out of bounds in %s at 0x%08X for jump table at 0x%08X\n", func, vram, jtbl);
assert(false);
exit(EXIT_FAILURE);
}
extern "C" void do_break(uint32_t vram) {
printf("Encountered break at original vram 0x%08X\n", vram);
assert(false);
exit(EXIT_FAILURE);
}
void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t arg) {
recomp_context ctx{};
ctx.r29 = sp;
ctx.r4 = arg;
ctx.mips3_float_mode = 0;
ctx.f_odd = &ctx.f0.u32h;
recomp_func_t* func = get_function(addr);
func(rdram, &ctx);
}
// Recomp generation functions
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
gpr get_entrypoint_address();
const char* get_rom_name();
void read_patch_data(uint8_t* rdram, gpr patch_data_address) {
for (size_t i = 0; i < sizeof(mm_patches_bin); i++) {
MEM_B(i, patch_data_address) = mm_patches_bin[i];
}
}
void init(uint8_t* rdram, recomp_context* ctx) {
// Initialize the overlays
init_overlays();
// Get entrypoint from recomp function
gpr entrypoint = get_entrypoint_address();
// Load overlays in the first 1MB
load_overlays(0x1000, (int32_t)entrypoint, 1024 * 1024);
// Initial 1MB DMA (rom address 0x1000 = physical address 0x10001000)
recomp::do_rom_read(rdram, entrypoint, 0x10001000, 0x100000);
// Read in any extra data from patches
read_patch_data(rdram, (gpr)(s32)0x80801000);
// Set up stack pointer
ctx->r29 = 0xFFFFFFFF803FFFF0u;
// Set up context floats
ctx->f_odd = &ctx->f0.u32h;
ctx->mips3_float_mode = false;
// Initialize variables normally set by IPL3
constexpr int32_t osTvType = 0x80000300;
constexpr int32_t osRomType = 0x80000304;
constexpr int32_t osRomBase = 0x80000308;
constexpr int32_t osResetType = 0x8000030c;
constexpr int32_t osCicId = 0x80000310;
constexpr int32_t osVersion = 0x80000314;
constexpr int32_t osMemSize = 0x80000318;
constexpr int32_t osAppNMIBuffer = 0x8000031c;
MEM_W(osTvType, 0) = 1; // NTSC
MEM_W(osRomBase, 0) = 0xB0000000u; // standard rom base
MEM_W(osResetType, 0) = 0; // cold reset
MEM_W(osMemSize, 0) = 8 * 1024 * 1024; // 8MB
}
std::atomic<recomp::Game> game_started = recomp::Game::None;
void recomp::start_game(recomp::Game game) {
game_started.store(game);
game_started.notify_all();
}
bool ultramodern::is_game_started() {
return game_started.load() != recomp::Game::None;
}
void set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks);
void set_input_callbacks(const ultramodern::input_callbacks_t& callback);
std::atomic_bool exited = false;
void ultramodern::quit() {
exited.store(true);
recomp::Game desired = recomp::Game::None;
game_started.compare_exchange_strong(desired, recomp::Game::Quit);
game_started.notify_all();
}
void recomp::start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks_) {
recomp::check_all_stored_roms();
set_audio_callbacks(audio_callbacks);
set_input_callbacks(input_callbacks);
ultramodern::gfx_callbacks_t gfx_callbacks = gfx_callbacks_;
ultramodern::gfx_callbacks_t::gfx_data_t gfx_data{};
if (gfx_callbacks.create_gfx) {
gfx_data = gfx_callbacks.create_gfx();
}
if (window_handle == ultramodern::WindowHandle{}) {
if (gfx_callbacks.create_window) {
window_handle = gfx_callbacks.create_window(gfx_data);
}
else {
assert(false && "No create_window callback provided");
}
}
// Allocate rdram_buffer
std::unique_ptr<uint8_t[]> rdram_buffer = std::make_unique<uint8_t[]>(ultramodern::rdram_size);
std::memset(rdram_buffer.get(), 0, ultramodern::rdram_size);
std::thread game_thread{[](ultramodern::WindowHandle window_handle, uint8_t* rdram) {
debug_printf("[Recomp] Starting\n");
ultramodern::set_native_thread_name("Game Start Thread");
ultramodern::preinit(rdram, window_handle);
game_started.wait(recomp::Game::None);
recomp_context context{};
switch (game_started.load()) {
case recomp::Game::MM:
if (!recomp::load_stored_rom(recomp::Game::MM)) {
recomp::message_box("Error opening stored ROM! Please restart this program.");
}
#ifdef HAS_MM_SHADER_CACHE
ultramodern::load_shader_cache({mm_shader_cache_bytes, sizeof(mm_shader_cache_bytes)});
#endif
init(rdram, &context);
try {
recomp_entrypoint(rdram, &context);
} catch (ultramodern::thread_terminated& terminated) {
}
break;
case recomp::Game::Quit:
break;
}
debug_printf("[Recomp] Quitting\n");
}, window_handle, rdram_buffer.get()};
while (!exited) {
ultramodern::sleep_milliseconds(1);
if (gfx_callbacks.update_gfx != nullptr) {
gfx_callbacks.update_gfx(gfx_data);
}
}
game_thread.join();
ultramodern::join_event_threads();
ultramodern::join_thread_cleaner_thread();
ultramodern::join_saving_thread();
}

View File

@ -1,50 +0,0 @@
#include <cstdio>
#include <fstream>
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osSpTaskLoad_recomp(uint8_t* rdram, recomp_context* ctx) {
// Nothing to do here
}
bool dump_frame = false;
extern "C" void osSpTaskStartGo_recomp(uint8_t* rdram, recomp_context* ctx) {
//printf("[sp] osSpTaskStartGo(0x%08X)\n", (uint32_t)ctx->r4);
OSTask* task = TO_PTR(OSTask, ctx->r4);
if (task->t.type == M_GFXTASK) {
//printf("[sp] Gfx task: %08X\n", (uint32_t)ctx->r4);
} else if (task->t.type == M_AUDTASK) {
//printf("[sp] Audio task: %08X\n", (uint32_t)ctx->r4);
}
// For debugging
if (dump_frame) {
char addr_str[32];
constexpr size_t ram_size = 0x800000;
std::unique_ptr<char[]> ram_unswapped = std::make_unique<char[]>(ram_size);
snprintf(addr_str, sizeof(addr_str) - 1, "%08X", task->t.data_ptr);
addr_str[sizeof(addr_str) - 1] = '\0';
std::ofstream dump_file{ "ramdump" + std::string{ addr_str } + ".bin", std::ios::binary};
for (size_t i = 0; i < ram_size; i++) {
ram_unswapped[i] = rdram[i ^ 3];
}
dump_file.write(ram_unswapped.get(), ram_size);
dump_frame = false;
}
ultramodern::submit_rsp_task(rdram, ctx->r4);
}
extern "C" void osSpTaskYield_recomp(uint8_t* rdram, recomp_context* ctx) {
// Ignore yield requests (acts as if the task completed before it received the yield request)
}
extern "C" void osSpTaskYielded_recomp(uint8_t* rdram, recomp_context* ctx) {
// Task yield requests are ignored, so always return 0 as tasks will never be yielded
ctx->r2 = 0;
}
extern "C" void __osSpSetPc_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);
}

View File

@ -1,45 +0,0 @@
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
// None of these functions need to be reimplemented, so stub them out
extern "C" void osUnmapTLBAll_recomp(uint8_t * rdram, recomp_context * ctx) {
// TODO this will need to be implemented in the future for any games that actually use the TLB
}
extern "C" void osVoiceInit_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 11; // CONT_ERR_DEVICE
}
extern "C" void osVoiceSetWord_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceCheckWord_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceStopReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceMaskDictionary_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceStartReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceControlGain_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceGetReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceClearDictionary_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}

View File

@ -1,163 +0,0 @@
#include <memory>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osInitialize_recomp(uint8_t * rdram, recomp_context * ctx) {
osInitialize();
}
extern "C" void __osInitialize_common_recomp(uint8_t * rdram, recomp_context * ctx) {
osInitialize();
}
extern "C" void osCreateThread_recomp(uint8_t* rdram, recomp_context* ctx) {
osCreateThread(rdram, (int32_t)ctx->r4, (OSId)ctx->r5, (int32_t)ctx->r6, (int32_t)ctx->r7,
(int32_t)MEM_W(0x10, ctx->r29), (OSPri)MEM_W(0x14, ctx->r29));
}
extern "C" void osStartThread_recomp(uint8_t* rdram, recomp_context* ctx) {
osStartThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osStopThread_recomp(uint8_t * rdram, recomp_context * ctx) {
osStopThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osDestroyThread_recomp(uint8_t * rdram, recomp_context * ctx) {
osDestroyThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osYieldThread_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
// osYieldThread(rdram);
}
extern "C" void osSetThreadPri_recomp(uint8_t* rdram, recomp_context* ctx) {
osSetThreadPri(rdram, (int32_t)ctx->r4, (OSPri)ctx->r5);
}
extern "C" void osGetThreadPri_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetThreadPri(rdram, (int32_t)ctx->r4);
}
extern "C" void osGetThreadId_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetThreadId(rdram, (int32_t)ctx->r4);
}
extern "C" void osCreateMesgQueue_recomp(uint8_t* rdram, recomp_context* ctx) {
osCreateMesgQueue(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6);
}
extern "C" void osRecvMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osRecvMesg(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6);
}
extern "C" void osSendMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osSendMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6);
}
extern "C" void osJamMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osJamMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6);
}
extern "C" void osSetEventMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
osSetEventMesg(rdram, (OSEvent)ctx->r4, (int32_t)ctx->r5, (OSMesg)ctx->r6);
}
extern "C" void osViSetEvent_recomp(uint8_t * rdram, recomp_context * ctx) {
osViSetEvent(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (u32)ctx->r6);
}
extern "C" void osGetCount_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetCount();
}
extern "C" void osGetTime_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t total_count = osGetTime();
ctx->r2 = (int32_t)(total_count >> 32);
ctx->r3 = (int32_t)(total_count >> 0);
}
extern "C" void osSetTimer_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t countdown = ((uint64_t)(ctx->r6) << 32) | ((ctx->r7) & 0xFFFFFFFFu);
uint64_t interval = load_doubleword(rdram, ctx->r29, 0x10);
ctx->r2 = osSetTimer(rdram, (int32_t)ctx->r4, countdown, interval, (int32_t)MEM_W(0x18, ctx->r29), (OSMesg)MEM_W(0x1C, ctx->r29));
}
extern "C" void osStopTimer_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osStopTimer(rdram, (int32_t)ctx->r4);
}
extern "C" void osVirtualToPhysical_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osVirtualToPhysical((int32_t)ctx->r4);
}
extern "C" void osInvalDCache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osInvalICache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osWritebackDCache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osWritebackDCacheAll_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osSetIntMask_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osDisableInt_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osRestoreInt_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osSetFpcCsr_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
// For the Mario Party games (not working)
//extern "C" void longjmp_recomp(uint8_t * rdram, recomp_context * ctx) {
// RecompJmpBuf* buf = TO_PTR(RecompJmpBuf, ctx->r4);
//
// // Check if this is a buffer that was set up with setjmp
// if (buf->magic == SETJMP_MAGIC) {
// // If so, longjmp to it
// // Setjmp/longjmp does not work across threads, so verify that this buffer was made by this thread
// assert(buf->owner == ultramodern::this_thread());
// longjmp(buf->storage->buffer, ctx->r5);
// } else {
// // Otherwise, check if it was one built manually by the game with $ra pointing to a function
// gpr sp = MEM_W(0, ctx->r4);
// gpr ra = MEM_W(4, ctx->r4);
// ctx->r29 = sp;
// recomp_func_t* target = LOOKUP_FUNC(ra);
// if (target == nullptr) {
// fprintf(stderr, "Failed to find function for manual longjmp\n");
// std::quick_exit(EXIT_FAILURE);
// }
// target(rdram, ctx);
//
// // TODO kill this thread if the target function returns
// assert(false);
// }
//}
//
//#undef setjmp_recomp
//extern "C" void setjmp_recomp(uint8_t * rdram, recomp_context * ctx) {
// fprintf(stderr, "Program called setjmp_recomp\n");
// std::quick_exit(EXIT_FAILURE);
//}
//
//extern "C" int32_t osGetThreadEx(void) {
// return ultramodern::this_thread();
//}

View File

@ -1,47 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osViSetYScale_recomp(uint8_t* rdram, recomp_context * ctx) {
osViSetYScale(ctx->f12.fl);
}
extern "C" void osViSetXScale_recomp(uint8_t* rdram, recomp_context * ctx) {
osViSetXScale(ctx->f12.fl);
}
extern "C" void osCreateViManager_recomp(uint8_t* rdram, recomp_context* ctx) {
;
}
extern "C" void osViBlack_recomp(uint8_t* rdram, recomp_context* ctx) {
osViBlack((uint32_t)ctx->r4);
}
extern "C" void osViSetSpecialFeatures_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSetSpecialFeatures((uint32_t)ctx->r4);
}
extern "C" void osViGetCurrentFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = (gpr)(int32_t)osViGetCurrentFramebuffer();
}
extern "C" void osViGetNextFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = (gpr)(int32_t)osViGetNextFramebuffer();
}
extern "C" void osViSwapBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSwapBuffer(rdram, (int32_t)ctx->r4);
}
extern "C" void osViSetMode_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSetMode(rdram, (int32_t)ctx->r4);
}
extern uint64_t total_vis;
extern "C" void wait_one_frame(uint8_t* rdram, recomp_context* ctx) {
uint64_t cur_vis = total_vis;
while (cur_vis == total_vis) {
std::this_thread::yield();
}
}

View File

@ -6,7 +6,7 @@
using ColourMap = Rml::UnorderedMap<Rml::String, Rml::Colourb>;
namespace recomp {
namespace recompui {
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();

View File

@ -1,24 +1,24 @@
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_sound.h"
#include "recomp_config.h"
#include "recomp_debug.h"
#include "zelda_sound.h"
#include "zelda_config.h"
#include "zelda_debug.h"
#include "zelda_render.h"
#include "promptfont.h"
#include "../../ultramodern/config.hpp"
#include "../../ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "rt64_layer.h"
ultramodern::GraphicsConfig new_options;
ultramodern::renderer::GraphicsConfig new_options;
Rml::DataModelHandle nav_help_model_handle;
Rml::DataModelHandle general_model_handle;
Rml::DataModelHandle controls_model_handle;
Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle sound_options_model_handle;
recomp::PromptContext prompt_context;
recompui::PromptContext prompt_context;
namespace recomp {
namespace recompui {
const std::unordered_map<ButtonVariant, std::string> button_variants {
{ButtonVariant::Primary, "primary"},
{ButtonVariant::Secondary, "secondary"},
@ -164,7 +164,7 @@ static bool cont_active = true;
static recomp::InputDevice cur_device = recomp::InputDevice::Controller;
void recomp::finish_scanning_input(recomp::InputField scanned_field) {
recomp::set_input_binding(static_cast<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
recomp::set_input_binding(static_cast<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
@ -173,7 +173,7 @@ void recomp::finish_scanning_input(recomp::InputField scanned_field) {
}
void recomp::cancel_scanning_input() {
recomp::stop_scanning_input();
recomp::stop_scanning_input();
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
@ -198,13 +198,13 @@ void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
}
void close_config_menu_impl() {
recomp::save_config();
zelda64::save_config();
if (ultramodern::is_game_started()) {
recomp::set_current_menu(recomp::Menu::None);
recompui::set_current_menu(recompui::Menu::None);
}
else {
recomp::set_current_menu(recomp::Menu::Launcher);
recompui::set_current_menu(recompui::Menu::Launcher);
}
}
@ -214,9 +214,9 @@ extern SDL_Window* window;
#endif
void apply_graphics_config(void) {
ultramodern::set_graphics_config(new_options);
ultramodern::renderer::set_graphics_config(new_options);
#if defined(__linux__) // TODO: Remove once RT64 gets native fullscreen support on Linux
if (new_options.wm_option == ultramodern::WindowMode::Fullscreen) {
if (new_options.wm_option == ultramodern::renderer::WindowMode::Fullscreen) {
SDL_SetWindowFullscreen(window,SDL_WINDOW_FULLSCREEN_DESKTOP);
} else {
SDL_SetWindowFullscreen(window,0);
@ -225,7 +225,7 @@ void apply_graphics_config(void) {
}
void close_config_menu() {
if (ultramodern::get_graphics_config() != new_options) {
if (ultramodern::renderer::get_graphics_config() != new_options) {
prompt_context.open_prompt(
"Graphics options have changed",
"Would you like to apply or discard the changes?",
@ -237,12 +237,12 @@ void close_config_menu() {
close_config_menu_impl();
},
[]() {
new_options = ultramodern::get_graphics_config();
new_options = ultramodern::renderer::get_graphics_config();
graphics_model_handle.DirtyAllVariables();
close_config_menu_impl();
},
recomp::ButtonVariant::Success,
recomp::ButtonVariant::Error,
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
"config__close-menu-button"
);
@ -252,7 +252,7 @@ void close_config_menu() {
close_config_menu_impl();
}
void recomp::open_quit_game_prompt() {
void zelda64::open_quit_game_prompt() {
prompt_context.open_prompt(
"Are you sure you want to quit?",
"Any progress since your last save will be lost.",
@ -262,8 +262,8 @@ void recomp::open_quit_game_prompt() {
ultramodern::quit();
},
[]() {},
recomp::ButtonVariant::Error,
recomp::ButtonVariant::Tertiary,
recompui::ButtonVariant::Error,
recompui::ButtonVariant::Tertiary,
true,
"config__quit-game-button"
);
@ -275,12 +275,12 @@ struct ControlOptionsContext {
int gyro_sensitivity; // 0 to 100
int mouse_sensitivity; // 0 to 100
int joystick_deadzone; // 0 to 100
recomp::TargetingMode targeting_mode;
zelda64::TargetingMode targeting_mode;
recomp::BackgroundInputMode background_input_mode;
recomp::AutosaveMode autosave_mode;
recomp::CameraInvertMode camera_invert_mode;
recomp::AnalogCamMode analog_cam_mode;
recomp::CameraInvertMode analog_camera_invert_mode;
zelda64::AutosaveMode autosave_mode;
zelda64::CameraInvertMode camera_invert_mode;
zelda64::AnalogCamMode analog_cam_mode;
zelda64::CameraInvertMode analog_camera_invert_mode;
};
ControlOptionsContext control_options_context;
@ -329,11 +329,11 @@ void recomp::set_joystick_deadzone(int deadzone) {
}
}
recomp::TargetingMode recomp::get_targeting_mode() {
zelda64::TargetingMode zelda64::get_targeting_mode() {
return control_options_context.targeting_mode;
}
void recomp::set_targeting_mode(recomp::TargetingMode mode) {
void zelda64::set_targeting_mode(zelda64::TargetingMode mode) {
control_options_context.targeting_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("targeting_mode");
@ -357,44 +357,44 @@ void recomp::set_background_input_mode(recomp::BackgroundInputMode mode) {
);
}
recomp::AutosaveMode recomp::get_autosave_mode() {
zelda64::AutosaveMode zelda64::get_autosave_mode() {
return control_options_context.autosave_mode;
}
void recomp::set_autosave_mode(recomp::AutosaveMode mode) {
void zelda64::set_autosave_mode(zelda64::AutosaveMode mode) {
control_options_context.autosave_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("autosave_mode");
}
}
recomp::CameraInvertMode recomp::get_camera_invert_mode() {
zelda64::CameraInvertMode zelda64::get_camera_invert_mode() {
return control_options_context.camera_invert_mode;
}
void recomp::set_camera_invert_mode(recomp::CameraInvertMode mode) {
void zelda64::set_camera_invert_mode(zelda64::CameraInvertMode mode) {
control_options_context.camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("camera_invert_mode");
}
}
recomp::AnalogCamMode recomp::get_analog_cam_mode() {
zelda64::AnalogCamMode zelda64::get_analog_cam_mode() {
return control_options_context.analog_cam_mode;
}
void recomp::set_analog_cam_mode(recomp::AnalogCamMode mode) {
void zelda64::set_analog_cam_mode(zelda64::AnalogCamMode mode) {
control_options_context.analog_cam_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_cam_mode");
}
}
recomp::CameraInvertMode recomp::get_analog_camera_invert_mode() {
zelda64::CameraInvertMode zelda64::get_analog_camera_invert_mode() {
return control_options_context.analog_camera_invert_mode;
}
void recomp::set_analog_camera_invert_mode(recomp::CameraInvertMode mode) {
void zelda64::set_analog_camera_invert_mode(zelda64::CameraInvertMode mode) {
control_options_context.analog_camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_camera_invert_mode");
@ -417,43 +417,43 @@ struct SoundOptionsContext {
SoundOptionsContext sound_options_context;
void recomp::reset_sound_settings() {
void zelda64::reset_sound_settings() {
sound_options_context.reset();
if (sound_options_model_handle) {
sound_options_model_handle.DirtyAllVariables();
}
}
void recomp::set_main_volume(int volume) {
void zelda64::set_main_volume(int volume) {
sound_options_context.main_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("main_volume");
}
}
int recomp::get_main_volume() {
int zelda64::get_main_volume() {
return sound_options_context.main_volume.load();
}
void recomp::set_bgm_volume(int volume) {
void zelda64::set_bgm_volume(int volume) {
sound_options_context.bgm_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("bgm_volume");
}
}
int recomp::get_bgm_volume() {
int zelda64::get_bgm_volume() {
return sound_options_context.bgm_volume.load();
}
void recomp::set_low_health_beeps_enabled(bool enabled) {
void zelda64::set_low_health_beeps_enabled(bool enabled) {
sound_options_context.low_health_beeps_enabled.store((int)enabled);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("low_health_beeps_enabled");
}
}
bool recomp::get_low_health_beeps_enabled() {
bool zelda64::get_low_health_beeps_enabled() {
return (bool)sound_options_context.low_health_beeps_enabled.load();
}
@ -471,7 +471,7 @@ struct DebugContext {
bool debug_enabled = false;
DebugContext() {
for (const auto& area : recomp::game_warps) {
for (const auto& area : zelda64::game_warps) {
area_names.emplace_back(area.name);
}
update_warp_names();
@ -479,15 +479,15 @@ struct DebugContext {
void update_warp_names() {
scene_names.clear();
for (const auto& scene : recomp::game_warps[area_index].scenes) {
for (const auto& scene : zelda64::game_warps[area_index].scenes) {
scene_names.emplace_back(scene.name);
}
entrance_names = recomp::game_warps[area_index].scenes[scene_index].entrances;
entrance_names = zelda64::game_warps[area_index].scenes[scene_index].entrances;
}
};
void recomp::update_rml_display_refresh_rate() {
void recompui::update_rml_display_refresh_rate() {
static uint32_t lastRate = 0;
if (!graphics_model_handle) return;
@ -500,7 +500,7 @@ void recomp::update_rml_display_refresh_rate() {
DebugContext debug_context;
class ConfigMenu : public recomp::MenuController {
class ConfigMenu : public recompui::MenuController {
public:
ConfigMenu() {
@ -511,13 +511,13 @@ public:
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/config_menu.rml");
}
void register_events(recomp::UiEventListenerInstancer& listener) override {
recomp::register_event(listener, "apply_options",
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
[](const std::string& param, Rml::Event& event) {
graphics_model_handle.DirtyVariable("options_changed");
apply_graphics_config();
});
recomp::register_event(listener, "config_keydown",
recompui::register_event(listener, "config_keydown",
[](const std::string& param, Rml::Event& event) {
if (!prompt_context.open && event.GetId() == Rml::EventId::Keydown) {
auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
@ -533,23 +533,23 @@ public:
}
});
// This needs to be separate from `close_config_menu` so it ensures that the event is only on the target
recomp::register_event(listener, "close_config_menu_backdrop",
recompui::register_event(listener, "close_config_menu_backdrop",
[](const std::string& param, Rml::Event& event) {
if (event.GetPhase() == Rml::EventPhase::Target) {
close_config_menu();
}
});
recomp::register_event(listener, "close_config_menu",
recompui::register_event(listener, "close_config_menu",
[](const std::string& param, Rml::Event& event) {
close_config_menu();
});
recomp::register_event(listener, "open_quit_game_prompt",
recompui::register_event(listener, "open_quit_game_prompt",
[](const std::string& param, Rml::Event& event) {
recomp::open_quit_game_prompt();
zelda64::open_quit_game_prompt();
});
recomp::register_event(listener, "toggle_input_device",
recompui::register_event(listener, "toggle_input_device",
[](const std::string& param, Rml::Event& event) {
cur_device = cur_device == recomp::InputDevice::Controller
? recomp::InputDevice::Keyboard
@ -558,7 +558,7 @@ public:
controls_model_handle.DirtyVariable("inputs");
});
recomp::register_event(listener, "area_index_changed",
recompui::register_event(listener, "area_index_changed",
[](const std::string& param, Rml::Event& event) {
debug_context.area_index = event.GetParameter<int>("value", 0);
debug_context.scene_index = 0;
@ -570,7 +570,7 @@ public:
debug_context.model_handle.DirtyVariable("entrance_names");
});
recomp::register_event(listener, "scene_index_changed",
recompui::register_event(listener, "scene_index_changed",
[](const std::string& param, Rml::Event& event) {
debug_context.scene_index = event.GetParameter<int>("value", 0);
debug_context.entrance_index = 0;
@ -579,14 +579,14 @@ public:
debug_context.model_handle.DirtyVariable("entrance_names");
});
recomp::register_event(listener, "do_warp",
recompui::register_event(listener, "do_warp",
[](const std::string& param, Rml::Event& event) {
recomp::do_warp(debug_context.area_index, debug_context.scene_index, debug_context.entrance_index);
zelda64::do_warp(debug_context.area_index, debug_context.scene_index, debug_context.entrance_index);
});
recomp::register_event(listener, "set_time",
recompui::register_event(listener, "set_time",
[](const std::string& param, Rml::Event& event) {
recomp::set_time(debug_context.set_time_day, debug_context.set_time_hour, debug_context.set_time_minute);
zelda64::set_time(debug_context.set_time_day, debug_context.set_time_hour, debug_context.set_time_minute);
});
}
@ -612,7 +612,7 @@ public:
}
ultramodern::sleep_milliseconds(50);
new_options = ultramodern::get_graphics_config();
new_options = ultramodern::renderer::get_graphics_config();
bind_config_list_events(constructor);
constructor.BindFunc("res_option",
@ -639,7 +639,7 @@ public:
});
constructor.BindFunc("ds_option",
[](Rml::Variant& out) {
if (new_options.res_option == ultramodern::Resolution::Auto) {
if (new_options.res_option == ultramodern::renderer::Resolution::Auto) {
out = 1;
} else {
out = new_options.ds_option;
@ -658,23 +658,23 @@ public:
constructor.BindFunc("options_changed",
[](Rml::Variant& out) {
out = (ultramodern::get_graphics_config() != new_options);
out = (ultramodern::renderer::get_graphics_config() != new_options);
});
constructor.BindFunc("ds_info",
[](Rml::Variant& out) {
switch (new_options.res_option) {
default:
case ultramodern::Resolution::Auto:
case ultramodern::renderer::Resolution::Auto:
out = "Downsampling is not available at auto resolution";
return;
case ultramodern::Resolution::Original:
case ultramodern::renderer::Resolution::Original:
if (new_options.ds_option == 2) {
out = "Rendered in 480p and scaled to 240p";
} else if (new_options.ds_option == 4) {
out = "Rendered in 960p and scaled to 240p";
}
return;
case ultramodern::Resolution::Original2x:
case ultramodern::renderer::Resolution::Original2x:
if (new_options.ds_option == 2) {
out = "Rendered in 960p and scaled to 480p";
} else if (new_options.ds_option == 4) {
@ -722,7 +722,7 @@ public:
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
scanned_input_index = inputs.at(0).Get<size_t>();
scanned_binding_index = inputs.at(1).Get<size_t>();
recomp::start_scanning_input(cur_device);
recomp::start_scanning_input(cur_device);
model_handle.DirtyVariable("active_binding_input");
model_handle.DirtyVariable("active_binding_slot");
});
@ -730,18 +730,18 @@ public:
constructor.BindEventCallback("reset_input_bindings_to_defaults",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
if (cur_device == recomp::InputDevice::Controller) {
recomp::reset_cont_input_bindings();
zelda64::reset_cont_input_bindings();
} else {
recomp::reset_kb_input_bindings();
zelda64::reset_kb_input_bindings();
}
model_handle.DirtyAllVariables();
});
constructor.BindEventCallback("clear_input_bindings",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) {
recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{});
recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{});
}
model_handle.DirtyVariable("inputs");
});
@ -776,7 +776,7 @@ public:
virtual int Size(void* ptr) override { return recomp::bindings_per_input; }
virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override {
recomp::GameInput input = static_cast<recomp::GameInput>((uintptr_t)ptr);
recomp::GameInput input = static_cast<recomp::GameInput>((uintptr_t)ptr);
return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(input, address.index, cur_device)};
}
};
@ -812,7 +812,7 @@ public:
return Rml::DataVariable(&binding_array_var_instance, nullptr);
}
else {
recomp::GameInput input = recomp::get_input_from_enum_name(address.name);
recomp::GameInput input = recomp::get_input_from_enum_name(address.name);
if (input != recomp::GameInput::COUNT) {
return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)input);
}
@ -966,14 +966,14 @@ public:
constructor.Bind("prompt__confirmLabel", &prompt_context.confirmLabel);
constructor.Bind("prompt__cancelLabel", &prompt_context.cancelLabel);
constructor.BindEventCallback("prompt__on_click", &recomp::PromptContext::on_click, &prompt_context);
constructor.BindEventCallback("prompt__on_click", &recompui::PromptContext::on_click, &prompt_context);
prompt_context.model_handle = constructor.GetModelHandle();
}
void make_bindings(Rml::Context* context) override {
// initially set cont state for ui help
recomp::config_menu_set_cont_or_kb(recomp::get_cont_active());
recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
make_nav_help_bindings(context);
make_general_bindings(context);
make_controls_bindings(context);
@ -984,34 +984,34 @@ public:
}
};
std::unique_ptr<recomp::MenuController> recomp::create_config_menu() {
std::unique_ptr<recompui::MenuController> recompui::create_config_menu() {
return std::make_unique<ConfigMenu>();
}
bool recomp::get_debug_mode_enabled() {
bool zelda64::get_debug_mode_enabled() {
return debug_context.debug_enabled;
}
void recomp::set_debug_mode_enabled(bool enabled) {
void zelda64::set_debug_mode_enabled(bool enabled) {
debug_context.debug_enabled = enabled;
if (debug_context.model_handle) {
debug_context.model_handle.DirtyVariable("debug_enabled");
}
}
void recomp::update_supported_options() {
msaa2x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA2X;
msaa4x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA4X;
msaa8x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA8X;
sample_positions_supported = ultramodern::RT64SamplePositionsSupported();
void recompui::update_supported_options() {
msaa2x_supported = zelda64::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA2X;
msaa4x_supported = zelda64::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA4X;
msaa8x_supported = zelda64::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA8X;
sample_positions_supported = zelda64::renderer::RT64SamplePositionsSupported();
new_options = ultramodern::get_graphics_config();
new_options = ultramodern::renderer::get_graphics_config();
graphics_model_handle.DirtyAllVariables();
}
void recomp::toggle_fullscreen() {
new_options.wm_option = (new_options.wm_option == ultramodern::WindowMode::Windowed) ? ultramodern::WindowMode::Fullscreen : ultramodern::WindowMode::Windowed;
void recompui::toggle_fullscreen() {
new_options.wm_option = (new_options.wm_option == ultramodern::renderer::WindowMode::Windowed) ? ultramodern::renderer::WindowMode::Fullscreen : ultramodern::renderer::WindowMode::Windowed;
apply_graphics_config();
graphics_model_handle.DirtyVariable("wm_option");
}

View File

@ -1,16 +1,18 @@
#include "recomp_ui.h"
#include "recomp_config.h"
#include "recomp_game.h"
#include "../../ultramodern/ultramodern.hpp"
#include "zelda_config.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "nfd.h"
#include <filesystem>
std::string version_number = "v1.1.0";
std::string version_number = "v1.1.1";
Rml::DataModelHandle model_handle;
bool mm_rom_valid = false;
extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
@ -21,39 +23,39 @@ void select_rom() {
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, recomp::Game::MM);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recomp::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recomp::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recomp::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recomp::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recomp::message_box("This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recomp::message_box("An unknown error has occurred.");
break;
}
}
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
}
class LauncherMenu : public recomp::MenuController {
class LauncherMenu : public recompui::MenuController {
public:
LauncherMenu() {
mm_rom_valid = recomp::is_rom_valid(recomp::Game::MM);
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
}
~LauncherMenu() override {
@ -61,37 +63,37 @@ public:
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/launcher.rml");
}
void register_events(recomp::UiEventListenerInstancer& listener) override {
recomp::register_event(listener, "select_rom",
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recomp::register_event(listener, "rom_selected",
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recomp::register_event(listener, "start_game",
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(recomp::Game::MM);
recomp::set_current_menu(recomp::Menu::None);
recomp::start_game(supported_games[0].game_id);
recompui::set_current_menu(recompui::Menu::None);
}
);
recomp::register_event(listener, "open_controls",
recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) {
recomp::set_current_menu(recomp::Menu::Config);
recomp::set_config_submenu(recomp::ConfigSubmenu::Controls);
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
}
);
recomp::register_event(listener, "open_settings",
recompui::register_event(listener, "open_settings",
[](const std::string& param, Rml::Event& event) {
recomp::set_current_menu(recomp::Menu::Config);
recomp::set_config_submenu(recomp::ConfigSubmenu::General);
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::General);
}
);
recomp::register_event(listener, "exit_game",
recompui::register_event(listener, "exit_game",
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
@ -107,6 +109,6 @@ public:
}
};
std::unique_ptr<recomp::MenuController> recomp::create_launcher_menu() {
std::unique_ptr<recompui::MenuController> recompui::create_launcher_menu() {
return std::make_unique<LauncherMenu>();
}

View File

@ -4,16 +4,21 @@
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <SDL_video.h>
#else
#include <SDL2/SDL_video.h>
#endif
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "ui_rml_hacks.hpp"
#include "concurrentqueue.h"
#include "rt64_layer.h"
#include "rt64_render_hooks.h"
#include "rt64_render_interface_builders.h"
@ -718,7 +723,7 @@ Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element)
return element;
}
namespace recomp {
namespace recompui {
class UiEventListener : public Rml::EventListener {
event_handler_t* handler_;
Rml::String param_;
@ -759,7 +764,7 @@ namespace recomp {
};
}
void recomp::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
listener.register_event(name, handler);
}
@ -779,8 +784,8 @@ Rml::Element* find_autofocus_element(Rml::Element* start) {
struct UIContext {
struct UIRenderContext render;
class {
std::unordered_map<recomp::Menu, std::unique_ptr<recomp::MenuController>> menus;
std::unordered_map<recomp::Menu, Rml::ElementDocument*> documents;
std::unordered_map<recompui::Menu, std::unique_ptr<recompui::MenuController>> menus;
std::unordered_map<recompui::Menu, Rml::ElementDocument*> documents;
Rml::ElementDocument* current_document;
Rml::Element* prev_focused;
bool mouse_is_active_changed = false;
@ -794,13 +799,13 @@ struct UIContext {
std::unique_ptr<SystemInterface_SDL> system_interface;
std::unique_ptr<RmlRenderInterface_RT64> render_interface;
Rml::Context* context;
recomp::UiEventListenerInstancer event_listener_instancer;
recompui::UiEventListenerInstancer event_listener_instancer;
void unload() {
render_interface.reset();
}
void swap_document(recomp::Menu menu) {
void swap_document(recompui::Menu menu) {
if (current_document != nullptr) {
Rml::Element* window_el = current_document->GetElementById("window");
if (window_el != nullptr) {
@ -831,7 +836,7 @@ struct UIContext {
mouse_is_active_initialized = false;
}
void swap_config_menu(recomp::ConfigSubmenu submenu) {
void swap_config_menu(recompui::ConfigSubmenu submenu) {
if (current_document != nullptr) {
Rml::Element* config_tabset_base = current_document->GetElementById("config_tabset");
if (config_tabset_base != nullptr) {
@ -911,7 +916,7 @@ struct UIContext {
}
if (mouse_is_active_initialized) {
recomp::set_cursor_visible(mouse_is_active);
recompui::set_cursor_visible(mouse_is_active);
}
if (current_document == nullptr) {
@ -939,7 +944,6 @@ struct UIContext {
if (cont_is_active || non_mouse_interacted) {
if (non_mouse_interacted) {
auto focusedEl = current_document->GetFocusLeafNode();
Rml::Variant* ti = focusedEl == nullptr ? nullptr : focusedEl->GetAttribute("tab-index");
if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
Rml::Element* element = find_autofocus_element(current_document);
if (element != nullptr) {
@ -981,14 +985,14 @@ struct UIContext {
}
}
void add_menu(recomp::Menu menu, std::unique_ptr<recomp::MenuController>&& controller) {
void add_menu(recompui::Menu menu, std::unique_ptr<recompui::MenuController>&& controller) {
menus.emplace(menu, std::move(controller));
}
void update_config_menu_loop(bool menu_changed) {
static int prevTab = -1;
if (menu_changed) prevTab = -1;
recomp::update_rml_display_refresh_rate();
recompui::update_rml_display_refresh_rate();
Rml::ElementTabSet *tabset = (Rml::ElementTabSet *)current_document->GetElementById("config_tabset");
if (tabset == nullptr) return;
@ -1022,7 +1026,7 @@ struct UIContext {
void update_prompt_loop(void) {
static bool wasShowingPrompt = false;
recomp::PromptContext *ctx = recomp::get_prompt_context();
recompui::PromptContext *ctx = recompui::get_prompt_context();
if (!ctx->open && wasShowingPrompt) {
Rml::Element* focused = current_document->GetFocusLeafNode();
if (focused) focused->Blur();
@ -1088,8 +1092,8 @@ struct UIContext {
Rml::Element *confirmButton = current_document->GetElementById("prompt__confirm-button");
Rml::Element *cancelButton = current_document->GetElementById("prompt__cancel-button");
if (confirmButton != nullptr) confirmButton->SetClassNames("button button--" + recomp::button_variants.at(ctx->confirmVariant));
if (cancelButton != nullptr) cancelButton->SetClassNames( "button button--" + recomp::button_variants.at(ctx->cancelVariant));
if (confirmButton != nullptr) confirmButton->SetClassNames("button button--" + recompui::button_variants.at(ctx->confirmVariant));
if (cancelButton != nullptr) cancelButton->SetClassNames( "button button--" + recompui::button_variants.at(ctx->cancelVariant));
}
} rml;
};
@ -1100,7 +1104,7 @@ std::mutex ui_context_mutex{};
// TODO make this not be global
extern SDL_Window* window;
void recomp::get_window_size(int& width, int& height) {
void recompui::get_window_size(int& width, int& height) {
SDL_GetWindowSizeInPixels(window, &width, &height);
}
@ -1110,8 +1114,8 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
#endif
ui_context = std::make_unique<UIContext>();
ui_context->rml.add_menu(recomp::Menu::Config, recomp::create_config_menu());
ui_context->rml.add_menu(recomp::Menu::Launcher, recomp::create_launcher_menu());
ui_context->rml.add_menu(recompui::Menu::Config, recompui::create_config_menu());
ui_context->rml.add_menu(recompui::Menu::Launcher, recompui::create_launcher_menu());
ui_context->render.interface = interface;
ui_context->render.device = device;
@ -1129,7 +1133,7 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
Rml::Initialise();
// Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
recomp::apply_color_hack();
recompui::apply_color_hack();
int width, height;
SDL_GetWindowSizeInPixels(window, &width, &height);
@ -1167,16 +1171,16 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
void recomp::queue_event(const SDL_Event& event) {
void recompui::queue_event(const SDL_Event& event) {
ui_event_queue.enqueue(event);
}
bool recomp::try_deque_event(SDL_Event& out) {
bool recompui::try_deque_event(SDL_Event& out) {
return ui_event_queue.try_dequeue(out);
}
std::atomic<recomp::Menu> open_menu = recomp::Menu::Launcher;
std::atomic<recomp::ConfigSubmenu> open_config_submenu = recomp::ConfigSubmenu::Count;
std::atomic<recompui::Menu> open_menu = recompui::Menu::Launcher;
std::atomic<recompui::ConfigSubmenu> open_config_submenu = recompui::ConfigSubmenu::Count;
int cont_button_to_key(SDL_ControllerButtonEvent& button) {
switch (button.button) {
@ -1236,15 +1240,15 @@ void apply_background_input_mode() {
last_input_mode = cur_input_mode;
}
bool recomp::get_cont_active() {
bool recompui::get_cont_active() {
return ui_context->rml.cont_is_active;
}
void recomp::set_cont_active(bool active) {
void recompui::set_cont_active(bool active) {
ui_context->rml.cont_is_active = active;
}
void recomp::activate_mouse() {
void recompui::activate_mouse() {
ui_context->rml.update_primary_input(true, false);
ui_context->rml.update_focus(true, false);
}
@ -1267,12 +1271,12 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool reload_sheets = is_reload_held && !was_reload_held;
was_reload_held = is_reload_held;
static recomp::Menu prev_menu = recomp::Menu::None;
recomp::Menu cur_menu = open_menu.load();
static recompui::Menu prev_menu = recompui::Menu::None;
recompui::Menu cur_menu = open_menu.load();
if (reload_sheets) {
ui_context->rml.load_documents();
prev_menu = recomp::Menu::None;
prev_menu = recompui::Menu::None;
}
bool menu_changed = cur_menu != prev_menu;
@ -1280,10 +1284,10 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
ui_context->rml.swap_document(cur_menu);
}
recomp::ConfigSubmenu config_submenu = open_config_submenu.load();
if (config_submenu != recomp::ConfigSubmenu::Count) {
recompui::ConfigSubmenu config_submenu = open_config_submenu.load();
if (config_submenu != recompui::ConfigSubmenu::Count) {
ui_context->rml.swap_config_menu(config_submenu);
open_config_submenu.store(recomp::ConfigSubmenu::Count);
open_config_submenu.store(recompui::ConfigSubmenu::Count);
}
prev_menu = cur_menu;
@ -1296,15 +1300,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool cont_interacted = false;
bool kb_interacted = false;
if (cur_menu == recomp::Menu::Config) {
if (cur_menu == recompui::Menu::Config) {
ui_context->rml.update_config_menu_loop(menu_changed);
}
if (cur_menu != recomp::Menu::None) {
if (cur_menu != recompui::Menu::None) {
ui_context->rml.update_prompt_loop();
}
while (recomp::try_deque_event(cur_event)) {
bool menu_is_open = cur_menu != recomp::Menu::None;
while (recompui::try_deque_event(cur_event)) {
bool menu_is_open = cur_menu != recompui::Menu::None;
if (!recomp::all_input_disabled()) {
// Implement some additional behavior for specific events on top of what RmlUi normally does with them.
@ -1323,7 +1327,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
last_mouse_pos[1] = cur_event.motion.y;
// if controller is the primary input, don't use mouse movement to allow cursor to reactivate
if (recomp::get_cont_active()) {
if (recompui::get_cont_active()) {
break;
}
}
@ -1401,15 +1405,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
if (open_config) {
cur_menu = recomp::Menu::Config;
open_menu.store(recomp::Menu::Config);
cur_menu = recompui::Menu::Config;
open_menu.store(recompui::Menu::Config);
ui_context->rml.swap_document(cur_menu);
}
}
} // end dequeue event loop
if (cont_interacted || kb_interacted || mouse_clicked) {
recomp::set_cont_active(cont_interacted);
recompui::set_cont_active(cont_interacted);
}
recomp::config_menu_set_cont_or_kb(ui_context->rml.cont_is_active);
@ -1421,7 +1425,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
ui_context->rml.update_primary_input(mouse_moved, non_mouse_interacted);
ui_context->rml.update_focus(mouse_moved, non_mouse_interacted);
if (cur_menu != recomp::Menu::None) {
if (cur_menu != recompui::Menu::None) {
int width = swap_chain_framebuffer->getWidth();
int height = swap_chain_framebuffer->getHeight();
@ -1453,29 +1457,29 @@ void deinit_hook() {
ui_context.reset();
}
void set_rt64_hooks() {
void recompui::set_render_hooks() {
RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
}
void recomp::set_current_menu(Menu menu) {
void recompui::set_current_menu(Menu menu) {
open_menu.store(menu);
if (menu == recomp::Menu::None) {
if (menu == recompui::Menu::None) {
ui_context->rml.system_interface->SetMouseCursor("arrow");
}
}
void recomp::set_config_submenu(recomp::ConfigSubmenu submenu) {
void recompui::set_config_submenu(recompui::ConfigSubmenu submenu) {
open_config_submenu.store(submenu);
}
void recomp::destroy_ui() {
void recompui::destroy_ui() {
}
recomp::Menu recomp::get_current_menu() {
recompui::Menu recompui::get_current_menu() {
return open_menu.load();
}
void recomp::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, recomp::program_name.data(), msg, nullptr);
void recompui::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
printf("[ERROR] %s\n", msg);
}

View File

@ -1,67 +0,0 @@
#include "ultra64.h"
#include "ultramodern.hpp"
#include <cassert>
static uint32_t sample_rate = 48000;
static ultramodern::audio_callbacks_t audio_callbacks;
void set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks) {
audio_callbacks = callbacks;
}
void ultramodern::init_audio() {
// Pick an initial dummy sample rate; this will be set by the game later to the true sample rate.
set_audio_frequency(48000);
}
void ultramodern::set_audio_frequency(uint32_t freq) {
if (audio_callbacks.set_frequency) {
audio_callbacks.set_frequency(freq);
}
sample_rate = freq;
}
void ultramodern::queue_audio_buffer(RDRAM_ARG PTR(int16_t) audio_data_, uint32_t byte_count) {
// Ensure that the byte count is an integer multiple of samples.
assert((byte_count & 1) == 0);
// Calculate the number of samples from the number of bytes.
uint32_t sample_count = byte_count / sizeof(int16_t);
// Queue the swapped audio data.
if (audio_callbacks.queue_samples) {
audio_callbacks.queue_samples(TO_PTR(int16_t, audio_data_), sample_count);
}
}
// For SDL2
//uint32_t buffer_offset_frames = 1;
// For Godot
float buffer_offset_frames = 0.5f;
// If there's ever any audio popping, check here first. Some games are very sensitive to
// the remaining sample count and reporting a number that's too high here can lead to issues.
// Reporting a number that's too low can lead to audio lag in some games.
uint32_t ultramodern::get_remaining_audio_bytes() {
// Get the number of remaining buffered audio bytes.
uint32_t buffered_byte_count;
if (audio_callbacks.get_frames_remaining != nullptr) {
buffered_byte_count = audio_callbacks.get_frames_remaining() * 2 * sizeof(int16_t);
}
else {
buffered_byte_count = 100;
}
// Adjust the reported count to be some number of refreshes in the future, which helps ensure that
// there are enough samples even if the audio thread experiences a small amount of lag. This prevents
// audio popping on games that use the buffered audio byte count to determine how many samples
// to generate.
uint32_t samples_per_vi = (sample_rate / 60);
if (buffered_byte_count > static_cast<uint32_t>(buffer_offset_frames * sizeof(int16_t) * samples_per_vi)) {
buffered_byte_count -= static_cast<uint32_t>(buffer_offset_frames * sizeof(int16_t) * samples_per_vi);
}
else {
buffered_byte_count = 0;
}
return buffered_byte_count;
}

View File

@ -1,86 +0,0 @@
#ifndef __CONFIG_HPP__
#define __CONFIG_HPP__
#include "common/rt64_user_configuration.h"
namespace ultramodern {
enum class Resolution {
Original,
Original2x,
Auto,
OptionCount
};
enum class WindowMode {
Windowed,
Fullscreen,
OptionCount
};
enum class HUDRatioMode {
Original,
Clamp16x9,
Full,
OptionCount
};
enum class GraphicsApi {
Auto,
D3D12,
Vulkan,
OptionCount
};
enum class HighPrecisionFramebuffer {
Auto,
On,
Off,
OptionCount
};
struct GraphicsConfig {
Resolution res_option;
WindowMode wm_option;
HUDRatioMode hr_option;
GraphicsApi api_option;
RT64::UserConfiguration::AspectRatio ar_option;
RT64::UserConfiguration::Antialiasing msaa_option;
RT64::UserConfiguration::RefreshRate rr_option;
HighPrecisionFramebuffer hpfb_option;
int rr_manual_value;
int ds_option;
bool developer_mode;
auto operator<=>(const GraphicsConfig& rhs) const = default;
};
void set_graphics_config(const GraphicsConfig& config);
GraphicsConfig get_graphics_config();
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::Resolution, {
{ultramodern::Resolution::Original, "Original"},
{ultramodern::Resolution::Original2x, "Original2x"},
{ultramodern::Resolution::Auto, "Auto"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::WindowMode, {
{ultramodern::WindowMode::Windowed, "Windowed"},
{ultramodern::WindowMode::Fullscreen, "Fullscreen"}
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::HUDRatioMode, {
{ultramodern::HUDRatioMode::Original, "Original"},
{ultramodern::HUDRatioMode::Clamp16x9, "Clamp16x9"},
{ultramodern::HUDRatioMode::Full, "Full"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::GraphicsApi, {
{ultramodern::GraphicsApi::Auto, "Auto"},
{ultramodern::GraphicsApi::D3D12, "D3D12"},
{ultramodern::GraphicsApi::Vulkan, "Vulkan"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::HighPrecisionFramebuffer, {
{ultramodern::HighPrecisionFramebuffer::Auto, "Auto"},
{ultramodern::HighPrecisionFramebuffer::On, "On"},
{ultramodern::HighPrecisionFramebuffer::Off, "Off"},
});
};
#endif

View File

@ -1,620 +0,0 @@
#include <thread>
#include <atomic>
#include <chrono>
#include <cinttypes>
#include <variant>
#include <unordered_map>
#include <utility>
#include <mutex>
#include <queue>
#include <cstring>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#include "config.hpp"
#include "rt64_layer.h"
#include "recomp.h"
#include "recomp_game.h"
#include "recomp_ui.h"
#include "recomp_input.h"
#include "rsp.h"
struct SpTaskAction {
OSTask task;
};
struct SwapBuffersAction {
uint32_t origin;
};
struct UpdateConfigAction {
};
struct LoadShaderCacheAction {
std::span<const char> data;
};
using Action = std::variant<SpTaskAction, SwapBuffersAction, UpdateConfigAction, LoadShaderCacheAction>;
static struct {
struct {
std::thread thread;
PTR(OSMesgQueue) mq = NULLPTR;
PTR(void) current_buffer = NULLPTR;
PTR(void) next_buffer = NULLPTR;
OSMesg msg = (OSMesg)0;
int retrace_count = 1;
} vi;
struct {
std::thread gfx_thread;
std::thread task_thread;
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} sp;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} dp;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} ai;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} si;
// The same message queue may be used for multiple events, so share a mutex for all of them
std::mutex message_mutex;
uint8_t* rdram;
moodycamel::BlockingConcurrentQueue<Action> action_queue{};
moodycamel::BlockingConcurrentQueue<OSTask*> sp_task_queue{};
moodycamel::ConcurrentQueue<OSThread*> deleted_threads{};
} events_context{};
extern "C" void osSetEventMesg(RDRAM_ARG OSEvent event_id, PTR(OSMesgQueue) mq_, OSMesg msg) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
std::lock_guard lock{ events_context.message_mutex };
switch (event_id) {
case OS_EVENT_SP:
events_context.sp.msg = msg;
events_context.sp.mq = mq_;
break;
case OS_EVENT_DP:
events_context.dp.msg = msg;
events_context.dp.mq = mq_;
break;
case OS_EVENT_AI:
events_context.ai.msg = msg;
events_context.ai.mq = mq_;
break;
case OS_EVENT_SI:
events_context.si.msg = msg;
events_context.si.mq = mq_;
}
}
extern "C" void osViSetEvent(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, u32 retrace_count) {
std::lock_guard lock{ events_context.message_mutex };
events_context.vi.mq = mq_;
events_context.vi.msg = msg;
events_context.vi.retrace_count = retrace_count;
}
uint64_t total_vis = 0;
extern std::atomic_bool exited;
void set_dummy_vi();
void vi_thread_func() {
ultramodern::set_native_thread_name("VI Thread");
// This thread should be prioritized over every other thread in the application, as it's what allows
// the game to generate new audio and gfx lists.
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Critical);
using namespace std::chrono_literals;
int remaining_retraces = events_context.vi.retrace_count;
while (!exited) {
// Determine the next VI time (more accurate than adding 16ms each VI interrupt)
auto next = ultramodern::get_start() + (total_vis * 1000000us) / (60 * ultramodern::get_speed_multiplier());
//if (next > std::chrono::high_resolution_clock::now()) {
// printf("Sleeping for %" PRIu64 " us to get from %" PRIu64 " us to %" PRIu64 " us \n",
// (next - std::chrono::high_resolution_clock::now()) / 1us,
// (std::chrono::high_resolution_clock::now() - events_context.start) / 1us,
// (next - events_context.start) / 1us);
//} else {
// printf("No need to sleep\n");
//}
// Detect if there's more than a second to wait and wait a fixed amount instead for the next VI if so, as that usually means the system clock went back in time.
if (std::chrono::floor<std::chrono::seconds>(next - std::chrono::high_resolution_clock::now()) > 1s) {
// printf("Skipping the next VI wait\n");
next = std::chrono::high_resolution_clock::now();
}
ultramodern::sleep_until(next);
// Calculate how many VIs have passed
uint64_t new_total_vis = (ultramodern::time_since_start() * (60 * ultramodern::get_speed_multiplier()) / 1000ms) + 1;
if (new_total_vis > total_vis + 1) {
//printf("Skipped % " PRId64 " frames in VI interupt thread!\n", new_total_vis - total_vis - 1);
}
total_vis = new_total_vis;
remaining_retraces--;
{
std::lock_guard lock{ events_context.message_mutex };
uint8_t* rdram = events_context.rdram;
if (remaining_retraces == 0) {
remaining_retraces = events_context.vi.retrace_count;
if (ultramodern::is_game_started()) {
if (events_context.vi.mq != NULLPTR) {
if (osSendMesg(PASS_RDRAM events_context.vi.mq, events_context.vi.msg, OS_MESG_NOBLOCK) == -1) {
//printf("Game skipped a VI frame!\n");
}
}
}
else {
set_dummy_vi();
static bool swap = false;
uint32_t vi_origin = 0x400 + 0x280; // Skip initial RDRAM contents and add the usual origin offset
// Offset by one FB every other frame so RT64 continues drawing
if (swap) {
vi_origin += 0x25800;
}
osViSwapBuffer(rdram, vi_origin);
swap = !swap;
}
}
if (events_context.ai.mq != NULLPTR) {
if (osSendMesg(PASS_RDRAM events_context.ai.mq, events_context.ai.msg, OS_MESG_NOBLOCK) == -1) {
//printf("Game skipped a AI frame!\n");
}
}
}
// TODO move recomp code out of ultramodern.
recomp::update_rumble();
}
}
void sp_complete() {
uint8_t* rdram = events_context.rdram;
std::lock_guard lock{ events_context.message_mutex };
osSendMesg(PASS_RDRAM events_context.sp.mq, events_context.sp.msg, OS_MESG_NOBLOCK);
}
void dp_complete() {
uint8_t* rdram = events_context.rdram;
std::lock_guard lock{ events_context.message_mutex };
osSendMesg(PASS_RDRAM events_context.dp.mq, events_context.dp.msg, OS_MESG_NOBLOCK);
}
uint8_t dmem[0x1000];
uint16_t rspReciprocals[512];
uint16_t rspInverseSquareRoots[512];
using RspUcodeFunc = RspExitReason(uint8_t* rdram);
extern RspUcodeFunc njpgdspMain;
extern RspUcodeFunc aspMain;
// From Ares emulator. For license details, see rsp_vu.h
void rsp_constants_init() {
rspReciprocals[0] = u16(~0);
for (u16 index = 1; index < 512; index++) {
u64 a = index + 512;
u64 b = (u64(1) << 34) / a;
rspReciprocals[index] = u16((b + 1) >> 8);
}
for (u16 index = 0; index < 512; index++) {
u64 a = (index + 512) >> ((index % 2 == 1) ? 1 : 0);
u64 b = 1 << 17;
//find the largest b where b < 1.0 / sqrt(a)
while (a * (b + 1) * (b + 1) < (u64(1) << 44)) b++;
rspInverseSquareRoots[index] = u16(b >> 1);
}
}
// Runs a recompiled RSP microcode
void run_rsp_microcode(uint8_t* rdram, const OSTask* task, RspUcodeFunc* ucode_func) {
// Load the OSTask into DMEM
memcpy(&dmem[0xFC0], task, sizeof(OSTask));
// Load the ucode data into DMEM
dma_rdram_to_dmem(rdram, 0x0000, task->t.ucode_data, 0xF80 - 1);
// Run the ucode
RspExitReason exit_reason = ucode_func(rdram);
// Ensure that the ucode exited correctly
assert(exit_reason == RspExitReason::Broke);
}
void task_thread_func(uint8_t* rdram, moodycamel::LightweightSemaphore* thread_ready) {
ultramodern::set_native_thread_name("SP Task Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Normal);
// Notify the caller thread that this thread is ready.
thread_ready->signal();
while (true) {
// Wait until an RSP task has been sent
OSTask* task;
events_context.sp_task_queue.wait_dequeue(task);
if (task == nullptr) {
return;
}
// Run the correct function based on the task type
if (task->t.type == M_AUDTASK) {
run_rsp_microcode(rdram, task, aspMain);
}
else if (task->t.type == M_NJPEGTASK) {
run_rsp_microcode(rdram, task, njpgdspMain);
}
else {
fprintf(stderr, "Unknown task type: %" PRIu32 "\n", task->t.type);
assert(false);
std::quick_exit(EXIT_FAILURE);
}
// Tell the game that the RSP has completed
sp_complete();
}
}
static std::atomic<ultramodern::GraphicsConfig> cur_config{};
void ultramodern::set_graphics_config(const ultramodern::GraphicsConfig& config) {
cur_config = config;
events_context.action_queue.enqueue(UpdateConfigAction{});
}
ultramodern::GraphicsConfig ultramodern::get_graphics_config() {
return cur_config;
}
std::atomic_uint32_t display_refresh_rate = 60;
std::atomic<float> resolution_scale = 1.0f;
uint32_t ultramodern::get_target_framerate(uint32_t original) {
ultramodern::GraphicsConfig graphics_config = ultramodern::get_graphics_config();
switch (graphics_config.rr_option) {
case RT64::UserConfiguration::RefreshRate::Original:
default:
return original;
case RT64::UserConfiguration::RefreshRate::Manual:
return graphics_config.rr_manual_value;
case RT64::UserConfiguration::RefreshRate::Display:
return display_refresh_rate.load();
}
}
uint32_t ultramodern::get_display_refresh_rate() {
return display_refresh_rate.load();
}
float ultramodern::get_resolution_scale() {
return resolution_scale.load();
}
void ultramodern::load_shader_cache(std::span<const char> cache_data) {
events_context.action_queue.enqueue(LoadShaderCacheAction{cache_data});
}
std::atomic<ultramodern::RT64SetupResult> rt64_setup_result = ultramodern::RT64SetupResult::Success;
void gfx_thread_func(uint8_t* rdram, moodycamel::LightweightSemaphore* thread_ready, ultramodern::WindowHandle window_handle) {
bool enabled_instant_present = false;
using namespace std::chrono_literals;
ultramodern::set_native_thread_name("Gfx Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Normal);
ultramodern::GraphicsConfig old_config = ultramodern::get_graphics_config();
ultramodern::RT64Context rt64{rdram, window_handle, cur_config.load().developer_mode};
if (!rt64.valid()) {
// TODO move recomp code out of ultramodern.
rt64_setup_result.store(rt64.get_setup_result());
// Notify the caller thread that this thread is ready.
thread_ready->signal();
return;
}
// TODO move recomp code out of ultramodern.
recomp::update_supported_options();
rsp_constants_init();
// Notify the caller thread that this thread is ready.
thread_ready->signal();
while (!exited) {
// Try to pull an action from the queue
Action action;
if (events_context.action_queue.wait_dequeue_timed(action, 1ms)) {
// Determine the action type and act on it
if (const auto* task_action = std::get_if<SpTaskAction>(&action)) {
// Turn on instant present if the game has been started and it hasn't been turned on yet.
if (ultramodern::is_game_started() && !enabled_instant_present) {
rt64.enable_instant_present();
enabled_instant_present = true;
}
// Tell the game that the RSP completed instantly. This will allow it to queue other task types, but it won't
// start another graphics task until the RDP is also complete. Games usually preserve the RSP inputs until the RDP
// is finished as well, so sending this early shouldn't be an issue in most cases.
// If this causes issues then the logic can be replaced with responding to yield requests.
sp_complete();
ultramodern::measure_input_latency();
auto rt64_start = std::chrono::high_resolution_clock::now();
rt64.send_dl(&task_action->task);
auto rt64_end = std::chrono::high_resolution_clock::now();
dp_complete();
// printf("RT64 ProcessDList time: %d us\n", static_cast<u32>(std::chrono::duration_cast<std::chrono::microseconds>(rt64_end - rt64_start).count()));
}
else if (const auto* swap_action = std::get_if<SwapBuffersAction>(&action)) {
events_context.vi.current_buffer = events_context.vi.next_buffer;
rt64.update_screen(swap_action->origin);
display_refresh_rate = rt64.get_display_framerate();
resolution_scale = rt64.get_resolution_scale();
}
else if (const auto* config_action = std::get_if<UpdateConfigAction>(&action)) {
ultramodern::GraphicsConfig new_config = cur_config;
if (old_config != new_config) {
rt64.update_config(old_config, new_config);
old_config = new_config;
}
}
else if (const auto* load_shader_cache_action = std::get_if<LoadShaderCacheAction>(&action)) {
rt64.load_shader_cache(load_shader_cache_action->data);
}
}
}
// TODO move recomp code out of ultramodern.
recomp::destroy_ui();
rt64.shutdown();
}
extern unsigned int VI_STATUS_REG;
extern unsigned int VI_ORIGIN_REG;
extern unsigned int VI_WIDTH_REG;
extern unsigned int VI_INTR_REG;
extern unsigned int VI_V_CURRENT_LINE_REG;
extern unsigned int VI_TIMING_REG;
extern unsigned int VI_V_SYNC_REG;
extern unsigned int VI_H_SYNC_REG;
extern unsigned int VI_LEAP_REG;
extern unsigned int VI_H_START_REG;
extern unsigned int VI_V_START_REG;
extern unsigned int VI_V_BURST_REG;
extern unsigned int VI_X_SCALE_REG;
extern unsigned int VI_Y_SCALE_REG;
uint32_t hstart = 0;
uint32_t vi_origin_offset = 320 * sizeof(uint16_t);
bool vi_black = false;
void set_dummy_vi() {
VI_STATUS_REG = 0x311E;
VI_WIDTH_REG = 0x140;
VI_V_SYNC_REG = 0x20D;
VI_H_SYNC_REG = 0xC15;
VI_LEAP_REG = 0x0C150C15;
hstart = 0x006C02EC;
VI_X_SCALE_REG = 0x200;
VI_V_CURRENT_LINE_REG = 0x0;
vi_origin_offset = 0x280;
VI_Y_SCALE_REG = 0x400;
VI_V_START_REG = 0x2501FF;
VI_V_BURST_REG = 0xE0204;
VI_INTR_REG = 0x2;
}
extern "C" void osViSwapBuffer(RDRAM_ARG PTR(void) frameBufPtr) {
if (vi_black) {
VI_H_START_REG = 0;
} else {
VI_H_START_REG = hstart;
}
events_context.vi.next_buffer = frameBufPtr;
events_context.action_queue.enqueue(SwapBuffersAction{ osVirtualToPhysical(frameBufPtr) + vi_origin_offset });
}
extern "C" void osViSetMode(RDRAM_ARG PTR(OSViMode) mode_) {
OSViMode* mode = TO_PTR(OSViMode, mode_);
VI_STATUS_REG = mode->comRegs.ctrl;
VI_WIDTH_REG = mode->comRegs.width;
// burst
VI_V_SYNC_REG = mode->comRegs.vSync;
VI_H_SYNC_REG = mode->comRegs.hSync;
VI_LEAP_REG = mode->comRegs.leap;
hstart = mode->comRegs.hStart;
VI_X_SCALE_REG = mode->comRegs.xScale;
VI_V_CURRENT_LINE_REG = mode->comRegs.vCurrent;
// TODO swap these every VI to account for fields changing
vi_origin_offset = mode->fldRegs[0].origin;
VI_Y_SCALE_REG = mode->fldRegs[0].yScale;
VI_V_START_REG = mode->fldRegs[0].vStart;
VI_V_BURST_REG = mode->fldRegs[0].vBurst;
VI_INTR_REG = mode->fldRegs[0].vIntr;
}
#define VI_CTRL_TYPE_16 0x00002
#define VI_CTRL_TYPE_32 0x00003
#define VI_CTRL_GAMMA_DITHER_ON 0x00004
#define VI_CTRL_GAMMA_ON 0x00008
#define VI_CTRL_DIVOT_ON 0x00010
#define VI_CTRL_SERRATE_ON 0x00040
#define VI_CTRL_ANTIALIAS_MASK 0x00300
#define VI_CTRL_ANTIALIAS_MODE_1 0x00100
#define VI_CTRL_ANTIALIAS_MODE_2 0x00200
#define VI_CTRL_ANTIALIAS_MODE_3 0x00300
#define VI_CTRL_PIXEL_ADV_MASK 0x01000
#define VI_CTRL_PIXEL_ADV_1 0x01000
#define VI_CTRL_PIXEL_ADV_2 0x02000
#define VI_CTRL_PIXEL_ADV_3 0x03000
#define VI_CTRL_DITHER_FILTER_ON 0x10000
#define OS_VI_GAMMA_ON 0x0001
#define OS_VI_GAMMA_OFF 0x0002
#define OS_VI_GAMMA_DITHER_ON 0x0004
#define OS_VI_GAMMA_DITHER_OFF 0x0008
#define OS_VI_DIVOT_ON 0x0010
#define OS_VI_DIVOT_OFF 0x0020
#define OS_VI_DITHER_FILTER_ON 0x0040
#define OS_VI_DITHER_FILTER_OFF 0x0080
extern "C" void osViSetSpecialFeatures(uint32_t func) {
if ((func & OS_VI_GAMMA_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_GAMMA_ON;
}
if ((func & OS_VI_GAMMA_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_GAMMA_ON;
}
if ((func & OS_VI_GAMMA_DITHER_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_GAMMA_DITHER_ON;
}
if ((func & OS_VI_GAMMA_DITHER_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_GAMMA_DITHER_ON;
}
if ((func & OS_VI_DIVOT_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_DIVOT_ON;
}
if ((func & OS_VI_DIVOT_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_DIVOT_ON;
}
if ((func & OS_VI_DITHER_FILTER_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_DITHER_FILTER_ON;
VI_STATUS_REG &= ~VI_CTRL_ANTIALIAS_MASK;
}
if ((func & OS_VI_DITHER_FILTER_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_DITHER_FILTER_ON;
//VI_STATUS_REG |= __osViNext->modep->comRegs.ctrl & VI_CTRL_ANTIALIAS_MASK;
}
}
extern "C" void osViBlack(uint8_t active) {
vi_black = active;
}
extern "C" void osViSetXScale(float scale) {
if (scale != 1.0f) {
assert(false);
}
}
extern "C" void osViSetYScale(float scale) {
if (scale != 1.0f) {
assert(false);
}
}
extern "C" PTR(void) osViGetNextFramebuffer() {
return events_context.vi.next_buffer;
}
extern "C" PTR(void) osViGetCurrentFramebuffer() {
return events_context.vi.current_buffer;
}
void ultramodern::submit_rsp_task(RDRAM_ARG PTR(OSTask) task_) {
OSTask* task = TO_PTR(OSTask, task_);
// Send gfx tasks to the graphics action queue
if (task->t.type == M_GFXTASK) {
events_context.action_queue.enqueue(SpTaskAction{ *task });
}
// Set all other tasks as the RSP task
else {
events_context.sp_task_queue.enqueue(task);
}
}
void ultramodern::send_si_message(RDRAM_ARG1) {
osSendMesg(PASS_RDRAM events_context.si.mq, events_context.si.msg, OS_MESG_NOBLOCK);
}
std::string get_graphics_api_name(ultramodern::GraphicsApi api) {
if (api == ultramodern::GraphicsApi::Auto) {
#if defined(_WIN32)
api = ultramodern::GraphicsApi::D3D12;
#elif defined(__gnu_linux__)
api = ultramodern::GraphicsApi::Vulkan;
#else
static_assert(false && "Unimplemented")
#endif
}
switch (api) {
case ultramodern::GraphicsApi::D3D12:
return "D3D12";
case ultramodern::GraphicsApi::Vulkan:
return "Vulkan";
default:
return "[Unknown graphics API]";
}
}
void ultramodern::init_events(RDRAM_ARG ultramodern::WindowHandle window_handle) {
moodycamel::LightweightSemaphore gfx_thread_ready;
moodycamel::LightweightSemaphore task_thread_ready;
events_context.rdram = rdram;
events_context.sp.gfx_thread = std::thread{ gfx_thread_func, rdram, &gfx_thread_ready, window_handle };
events_context.sp.task_thread = std::thread{ task_thread_func, rdram, &task_thread_ready };
// Wait for the two sp threads to be ready before continuing to prevent the game from
// running before we're able to handle RSP tasks.
gfx_thread_ready.wait();
task_thread_ready.wait();
ultramodern::RT64SetupResult setup_result = rt64_setup_result.load();
if (rt64_setup_result != ultramodern::RT64SetupResult::Success) {
auto show_rt64_error = [](const std::string& msg) {
// TODO move recomp code out of ultramodern (message boxes).
recomp::message_box(("An error has been encountered on startup: " + msg).c_str());
};
const std::string driver_os_suffix = "\nPlease make sure your GPU drivers and your OS are up to date.";
switch (rt64_setup_result) {
case ultramodern::RT64SetupResult::DynamicLibrariesNotFound:
show_rt64_error("Failed to load dynamic libraries. Make sure the DLLs are next to the recomp executable.");
break;
case ultramodern::RT64SetupResult::InvalidGraphicsAPI:
show_rt64_error(get_graphics_api_name(cur_config.load().api_option) + " is not supported on this platform. Please select a different graphics API.");
break;
case ultramodern::RT64SetupResult::GraphicsAPINotFound:
show_rt64_error("Unable to initialize " + get_graphics_api_name(cur_config.load().api_option) + "." + driver_os_suffix);
break;
case ultramodern::RT64SetupResult::GraphicsDeviceNotFound:
show_rt64_error("Unable to find compatible graphics device." + driver_os_suffix);
break;
}
throw std::runtime_error("Failed to initialize RT64");
}
events_context.vi.thread = std::thread{ vi_thread_func };
}
void ultramodern::join_event_threads() {
events_context.sp.gfx_thread.join();
events_context.vi.thread.join();
// Send a null RSP task to indicate that the RSP task thread should exit.
events_context.sp_task_queue.enqueue(nullptr);
events_context.sp.task_thread.join();
}

View File

@ -1,188 +0,0 @@
#include <thread>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#include "recomp.h"
struct QueuedMessage {
PTR(OSMesgQueue) mq;
OSMesg mesg;
bool jam;
};
static moodycamel::BlockingConcurrentQueue<QueuedMessage> external_messages {};
void enqueue_external_message(PTR(OSMesgQueue) mq, OSMesg msg, bool jam) {
external_messages.enqueue({mq, msg, jam});
}
bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block);
void dequeue_external_messages(RDRAM_ARG1) {
QueuedMessage to_send;
while (external_messages.try_dequeue(to_send)) {
do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false);
}
}
void ultramodern::wait_for_external_message(RDRAM_ARG1) {
QueuedMessage to_send;
external_messages.wait_dequeue(to_send);
do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false);
}
extern "C" void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg, s32 count) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
mq->blocked_on_recv = NULLPTR;
mq->blocked_on_send = NULLPTR;
mq->msgCount = count;
mq->msg = msg;
mq->validCount = 0;
mq->first = 0;
}
s32 MQ_GET_COUNT(OSMesgQueue *mq) {
return mq->validCount;
}
s32 MQ_IS_EMPTY(OSMesgQueue *mq) {
return mq->validCount == 0;
}
s32 MQ_IS_FULL(OSMesgQueue* mq) {
return MQ_GET_COUNT(mq) >= mq->msgCount;
}
bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
if (!block) {
// If non-blocking, fail if the queue is full.
if (MQ_IS_FULL(mq)) {
return false;
}
}
else {
// Otherwise, yield this thread until the queue has room.
while (MQ_IS_FULL(mq)) {
debug_printf("[Message Queue] Thread %d is blocked on send\n", TO_PTR(OSThread, ultramodern::this_thread())->id);
ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_send), ultramodern::this_thread());
ultramodern::run_next_thread_and_wait(PASS_RDRAM1);
}
}
if (jam) {
// Jams insert at the head of the message queue's buffer.
mq->first = (mq->first + mq->msgCount - 1) % mq->msgCount;
TO_PTR(OSMesg, mq->msg)[mq->first] = msg;
mq->validCount++;
}
else {
// Sends insert at the tail of the message queue's buffer.
s32 last = (mq->first + mq->validCount) % mq->msgCount;
TO_PTR(OSMesg, mq->msg)[last] = msg;
mq->validCount++;
}
// If any threads were blocked on receiving from this message queue, pop the first one and schedule it.
PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv);
if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) {
ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue));
}
return true;
}
bool do_recv(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, bool block) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
if (!block) {
// If non-blocking, fail if the queue is empty
if (MQ_IS_EMPTY(mq)) {
return false;
}
} else {
// Otherwise, yield this thread in a loop until the queue is no longer full
while (MQ_IS_EMPTY(mq)) {
debug_printf("[Message Queue] Thread %d is blocked on receive\n", TO_PTR(OSThread, ultramodern::this_thread())->id);
ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv), ultramodern::this_thread());
ultramodern::run_next_thread_and_wait(PASS_RDRAM1);
}
}
if (msg_ != NULLPTR) {
*TO_PTR(OSMesg, msg_) = TO_PTR(OSMesg, mq->msg)[mq->first];
}
mq->first = (mq->first + 1) % mq->msgCount;
mq->validCount--;
// If any threads were blocked on sending to this message queue, pop the first one and schedule it.
PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_send);
if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) {
ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue));
}
return true;
}
extern "C" s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
bool jam = false;
// Don't directly send to the message queue if this isn't a game thread to avoid contention.
if (!ultramodern::is_game_thread()) {
enqueue_external_message(mq_, msg, jam);
return 0;
}
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to send the message.
bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return sent ? 0 : -1;
}
extern "C" s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
bool jam = true;
// Don't directly send to the message queue if this isn't a game thread to avoid contention.
if (!ultramodern::is_game_thread()) {
enqueue_external_message(mq_, msg, jam);
return 0;
}
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to send the message.
bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return sent ? 0 : -1;
}
extern "C" s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
assert(ultramodern::is_game_thread() && "RecvMesg not allowed outside of game threads.");
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to receive a message.
bool received = do_recv(PASS_RDRAM mq_, msg_, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return received ? 0 : -1;
}

View File

@ -1,22 +0,0 @@
#include "ultra64.h"
#define K0BASE 0x80000000
#define K1BASE 0xA0000000
#define K2BASE 0xC0000000
#define IS_KSEG0(x) ((u32)(x) >= K0BASE && (u32)(x) < K1BASE)
#define IS_KSEG1(x) ((u32)(x) >= K1BASE && (u32)(x) < K2BASE)
#define K0_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) /* kseg0 to physical */
#define K1_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) /* kseg1 to physical */
u32 osVirtualToPhysical(PTR(void) addr) {
uintptr_t addr_val = (uintptr_t)addr;
if (IS_KSEG0(addr_val)) {
return K0_TO_PHYS(addr_val);
} else if (IS_KSEG1(addr_val)) {
return K1_TO_PHYS(addr_val);
} else {
// TODO handle TLB mappings
return (u32)addr_val;
}
}

View File

@ -1,83 +0,0 @@
#if 0
#include <stdio.h>
#include <stdlib.h>
#include "ultra64.h"
#define THREAD_STACK_SIZE 0x1000
u8 idle_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 main_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 thread3_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 thread4_stack[THREAD_STACK_SIZE] ALIGNED(16);
OSThread idle_thread;
OSThread main_thread;
OSThread thread3;
OSThread thread4;
OSMesgQueue queue;
OSMesg buf[1];
void thread3_func(UNUSED void *arg) {
OSMesg val;
printf("Thread3 recv\n");
fflush(stdout);
osRecvMesg(&queue, &val, OS_MESG_BLOCK);
printf("Thread3 complete: %d\n", (int)(intptr_t)val);
fflush(stdout);
}
void thread4_func(void *arg) {
printf("Thread4 send %d\n", (int)(intptr_t)arg);
fflush(stdout);
osSendMesg(&queue, arg, OS_MESG_BLOCK);
printf("Thread4 complete\n");
fflush(stdout);
}
void main_thread_func(UNUSED void* arg) {
osCreateMesgQueue(&queue, buf, sizeof(buf) / sizeof(buf[0]));
printf("main thread creating thread 3\n");
osCreateThread(&thread3, 3, thread3_func, NULL, &thread3_stack[THREAD_STACK_SIZE], 14);
printf("main thread starting thread 3\n");
osStartThread(&thread3);
printf("main thread creating thread 4\n");
osCreateThread(&thread4, 4, thread4_func, (void*)10, &thread4_stack[THREAD_STACK_SIZE], 13);
printf("main thread starting thread 4\n");
osStartThread(&thread4);
while (1) {
printf("main thread doin stuff\n");
sleep(1);
}
}
void idle_thread_func(UNUSED void* arg) {
printf("idle thread\n");
printf("creating main thread\n");
osCreateThread(&main_thread, 2, main_thread_func, NULL, &main_stack[THREAD_STACK_SIZE], 11);
printf("starting main thread\n");
osStartThread(&main_thread);
// Set this thread's priority to 0, making it the idle thread
osSetThreadPri(NULL, 0);
// idle
while (1) {
printf("idle thread doin stuff\n");
sleep(1);
}
}
void bootproc(void) {
osInitialize();
osCreateThread(&idle_thread, 1, idle_thread_func, NULL, &idle_stack[THREAD_STACK_SIZE], 127);
printf("Starting idle thread\n");
osStartThread(&idle_thread);
}
#endif

View File

@ -1,38 +0,0 @@
#include "ultramodern.hpp"
void ultramodern::schedule_running_thread(RDRAM_ARG PTR(OSThread) t_) {
debug_printf("[Scheduling] Adding thread %d to the running queue\n", TO_PTR(OSThread, t_)->id);
thread_queue_insert(PASS_RDRAM running_queue, t_);
TO_PTR(OSThread, t_)->state = OSThreadState::QUEUED;
}
void swap_to_thread(RDRAM_ARG OSThread *to) {
debug_printf("[Scheduling] Thread %d giving execution to thread %d\n", TO_PTR(OSThread, ultramodern::this_thread())->id, to->id);
// Insert this thread in the running queue.
ultramodern::thread_queue_insert(PASS_RDRAM ultramodern::running_queue, ultramodern::this_thread());
TO_PTR(OSThread, ultramodern::this_thread())->state = OSThreadState::QUEUED;
// Unpause the target thread and wait for this one to be unpaused.
ultramodern::resume_thread_and_wait(PASS_RDRAM to);
}
void ultramodern::check_running_queue(RDRAM_ARG1) {
// Check if there are any threads in the running queue.
if (!thread_queue_empty(PASS_RDRAM running_queue)) {
// Check if the highest priority thread in the queue is higher priority than the current thread.
OSThread* next_thread = TO_PTR(OSThread, ultramodern::thread_queue_peek(PASS_RDRAM running_queue));
OSThread* self = TO_PTR(OSThread, ultramodern::this_thread());
if (next_thread->priority > self->priority) {
ultramodern::thread_queue_pop(PASS_RDRAM running_queue);
// Swap to the higher priority thread.
swap_to_thread(PASS_RDRAM next_thread);
}
}
}
extern "C" void pause_self(RDRAM_ARG1) {
while (true) {
// Wait until an external message arrives, then allow the next thread to run.
ultramodern::wait_for_external_message(PASS_RDRAM1);
ultramodern::check_running_queue(PASS_RDRAM1);
}
}

View File

@ -1,13 +0,0 @@
#ifdef _WIN32
#include <Windows.h>
#include "ultra64.h"
#include "ultramodern.hpp"
extern "C" unsigned int sleep(unsigned int seconds) {
Sleep(seconds * 1000);
return 0;
}
#endif

View File

@ -1,66 +0,0 @@
#include <cassert>
#include "ultramodern.hpp"
static PTR(OSThread) running_queue_impl = NULLPTR;
static PTR(OSThread)* queue_to_ptr(RDRAM_ARG PTR(PTR(OSThread)) queue) {
if (queue == ultramodern::running_queue) {
return &running_queue_impl;
}
return TO_PTR(PTR(OSThread), queue);
}
void ultramodern::thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) toadd_) {
PTR(OSThread)* cur = queue_to_ptr(PASS_RDRAM queue_);
OSThread* toadd = TO_PTR(OSThread, toadd_);
debug_printf("[Thread Queue] Inserting thread %d into queue 0x%08X\n", toadd->id, (uintptr_t)queue_);
while (*cur && TO_PTR(OSThread, *cur)->priority > toadd->priority) {
cur = &TO_PTR(OSThread, *cur)->next;
}
toadd->next = (*cur);
toadd->queue = queue_;
*cur = toadd_;
debug_printf(" Contains:");
cur = queue_to_ptr(PASS_RDRAM queue_);
while (*cur) {
debug_printf("%d (%d) ", TO_PTR(OSThread, *cur)->id, TO_PTR(OSThread, *cur)->priority);
cur = &TO_PTR(OSThread, *cur)->next;
}
debug_printf("\n");
}
PTR(OSThread) ultramodern::thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
PTR(OSThread) ret = *queue;
*queue = TO_PTR(OSThread, ret)->next;
TO_PTR(OSThread, ret)->queue = NULLPTR;
debug_printf("[Thread Queue] Popped thread %d from queue 0x%08X\n", TO_PTR(OSThread, ret)->id, (uintptr_t)queue_);
return ret;
}
bool ultramodern::thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_) {
debug_printf("[Thread Queue] Removing thread %d from queue 0x%08X\n", TO_PTR(OSThread, t_)->id, (uintptr_t)queue_);
PTR(PTR(OSThread)) cur = queue_;
while (cur != NULLPTR) {
PTR(OSThread)* cur_ptr = queue_to_ptr(PASS_RDRAM queue_);
if (*cur_ptr == t_) {
return true;
}
cur = TO_PTR(OSThread, *cur_ptr)->next;
}
return false;
}
bool ultramodern::thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
return *queue == NULLPTR;
}
PTR(OSThread) ultramodern::thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
return *queue;
}

View File

@ -1,346 +0,0 @@
#include <cstdio>
#include <thread>
#include <cassert>
#include <string>
#include "ultra64.h"
#include "ultramodern.hpp"
#include "blockingconcurrentqueue.h"
// Native APIs only used to set thread names for easier debugging
#ifdef _WIN32
#include <Windows.h>
#endif
extern "C" void bootproc();
thread_local bool is_main_thread = false;
// Whether this thread is part of the game (i.e. the start thread or one spawned by osCreateThread)
thread_local bool is_game_thread = false;
thread_local PTR(OSThread) thread_self = NULLPTR;
void ultramodern::set_main_thread() {
::is_game_thread = true;
is_main_thread = true;
}
bool ultramodern::is_game_thread() {
return ::is_game_thread;
}
#if 0
int main(int argc, char** argv) {
ultramodern::set_main_thread();
bootproc();
}
#endif
#if 1
void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t arg);
#else
#define run_thread_function(func, sp, arg) func(arg)
#endif
#if defined(_WIN32)
void ultramodern::set_native_thread_name(const std::string& name) {
std::wstring wname{name.begin(), name.end()};
HRESULT r;
r = SetThreadDescription(
GetCurrentThread(),
wname.c_str()
);
}
void ultramodern::set_native_thread_priority(ThreadPriority pri) {
int nPriority = THREAD_PRIORITY_NORMAL;
// Convert ThreadPriority to Win32 priority
switch (pri) {
case ThreadPriority::Low:
nPriority = THREAD_PRIORITY_BELOW_NORMAL;
break;
case ThreadPriority::Normal:
nPriority = THREAD_PRIORITY_NORMAL;
break;
case ThreadPriority::High:
nPriority = THREAD_PRIORITY_ABOVE_NORMAL;
break;
case ThreadPriority::VeryHigh:
nPriority = THREAD_PRIORITY_HIGHEST;
break;
case ThreadPriority::Critical:
nPriority = THREAD_PRIORITY_TIME_CRITICAL;
break;
default:
throw std::runtime_error("Invalid thread priority!");
break;
}
// SetThreadPriority(GetCurrentThread(), nPriority);
}
#elif defined(__linux__)
void ultramodern::set_native_thread_name(const std::string& name) {
pthread_setname_np(pthread_self(), name.c_str());
}
void ultramodern::set_native_thread_priority(ThreadPriority pri) {
// TODO linux thread priority
// printf("set_native_thread_priority unimplemented\n");
// int nPriority = THREAD_PRIORITY_NORMAL;
// // Convert ThreadPriority to Win32 priority
// switch (pri) {
// case ThreadPriority::Low:
// nPriority = THREAD_PRIORITY_BELOW_NORMAL;
// break;
// case ThreadPriority::Normal:
// nPriority = THREAD_PRIORITY_NORMAL;
// break;
// case ThreadPriority::High:
// nPriority = THREAD_PRIORITY_ABOVE_NORMAL;
// break;
// case ThreadPriority::VeryHigh:
// nPriority = THREAD_PRIORITY_HIGHEST;
// break;
// case ThreadPriority::Critical:
// nPriority = THREAD_PRIORITY_TIME_CRITICAL;
// break;
// default:
// throw std::runtime_error("Invalid thread priority!");
// break;
// }
}
#endif
std::atomic_int temporary_threads = 0;
std::atomic_int permanent_threads = 0;
void wait_for_resumed(RDRAM_ARG UltraThreadContext* thread_context) {
TO_PTR(OSThread, ultramodern::this_thread())->context->running.wait();
// If this thread's context was replaced by another thread or deleted, destroy it again from its own context.
// This will trigger thread cleanup instead.
if (TO_PTR(OSThread, ultramodern::this_thread())->context != thread_context) {
osDestroyThread(PASS_RDRAM NULLPTR);
}
}
void resume_thread(OSThread* t) {
debug_printf("[Thread] Resuming execution of thread %d\n", t->id);
t->context->running.signal();
}
void run_next_thread(RDRAM_ARG1) {
if (ultramodern::thread_queue_empty(PASS_RDRAM ultramodern::running_queue)) {
throw std::runtime_error("No threads left to run!\n");
}
OSThread* to_run = TO_PTR(OSThread, ultramodern::thread_queue_pop(PASS_RDRAM ultramodern::running_queue));
debug_printf("[Scheduling] Resuming execution of thread %d\n", to_run->id);
to_run->context->running.signal();
}
void ultramodern::run_next_thread_and_wait(RDRAM_ARG1) {
UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context;
run_next_thread(PASS_RDRAM1);
wait_for_resumed(PASS_RDRAM cur_context);
}
void ultramodern::resume_thread_and_wait(RDRAM_ARG OSThread *t) {
UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context;
resume_thread(t);
wait_for_resumed(PASS_RDRAM cur_context);
}
static void _thread_func(RDRAM_ARG PTR(OSThread) self_, PTR(thread_func_t) entrypoint, PTR(void) arg, UltraThreadContext* thread_context) {
OSThread *self = TO_PTR(OSThread, self_);
debug_printf("[Thread] Thread created: %d\n", self->id);
thread_self = self_;
is_game_thread = true;
// Set the thread name
ultramodern::set_native_thread_name("Game Thread " + std::to_string(self->id));
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::High);
// TODO fix these being hardcoded (this is only used for quicksaving)
if ((self->id == 2 && self->priority == 5) || self->id == 13) { // slowly, flashrom
temporary_threads.fetch_add(1);
}
else if (self->id != 1 && self->id != 2) { // ignore idle and fault
permanent_threads.fetch_add(1);
}
// Signal the initialized semaphore to indicate that this thread can be started.
thread_context->initialized.signal();
debug_printf("[Thread] Thread waiting to be started: %d\n", self->id);
// Wait until the thread is marked as running.
wait_for_resumed(PASS_RDRAM thread_context);
// Make sure the thread wasn't replaced or destroyed before it was started.
if (self->context == thread_context) {
debug_printf("[Thread] Thread started: %d\n", self->id);
try {
// Run the thread's function with the provided argument.
run_thread_function(PASS_RDRAM entrypoint, self->sp, arg);
} catch (ultramodern::thread_terminated& terminated) {
}
}
else {
debug_printf("[Thread] Thread destroyed before being started: %d\n", self->id);
}
// Check if the thread hasn't been destroyed or replaced. If so, then the thread terminated or destroyed itself,
// so mark this thread as destroyed and run the next queued thread.
if (self->context == thread_context) {
self->context = nullptr;
run_next_thread(PASS_RDRAM1);
}
// Dispose of this thread now that it's completed or terminated.
ultramodern::cleanup_thread(thread_context);
// TODO fix these being hardcoded (this is only used for quicksaving)
if ((self->id == 2 && self->priority == 5) || self->id == 13) { // slowly, flashrom
temporary_threads.fetch_sub(1);
}
}
uint32_t ultramodern::permanent_thread_count() {
return permanent_threads.load();
}
uint32_t ultramodern::temporary_thread_count() {
return temporary_threads.load();
}
extern "C" void osStartThread(RDRAM_ARG PTR(OSThread) t_) {
OSThread* t = TO_PTR(OSThread, t_);
debug_printf("[os] Start Thread %d\n", t->id);
// Wait until the thread is initialized to indicate that it's ready to be started.
t->context->initialized.wait();
debug_printf("[os] Thread %d is ready to be started\n", t->id);
// If this is a game thread, insert the new thread into the running queue and then check the running queue.
if (thread_self) {
ultramodern::schedule_running_thread(PASS_RDRAM t_);
ultramodern::check_running_queue(PASS_RDRAM1);
}
// Otherwise, immediately start the thread and terminate this one.
else {
t->state = OSThreadState::QUEUED;
resume_thread(t);
//throw ultramodern::thread_terminated{};
}
}
extern "C" void osCreateThread(RDRAM_ARG PTR(OSThread) t_, OSId id, PTR(thread_func_t) entrypoint, PTR(void) arg, PTR(void) sp, OSPri pri) {
debug_printf("[os] Create Thread %d\n", id);
OSThread *t = TO_PTR(OSThread, t_);
t->next = NULLPTR;
t->queue = NULLPTR;
t->priority = pri;
t->id = id;
t->state = OSThreadState::STOPPED;
t->sp = sp - 0x10; // Set up the first stack frame
// Spawn a new thread, which will immediately pause itself and wait until it's been started.
// Pass the context as an argument to the thread function to ensure that it can't get cleared before the thread captures its value.
t->context = new UltraThreadContext{};
t->context->host_thread = std::thread{_thread_func, PASS_RDRAM t_, entrypoint, arg, t->context};
}
extern "C" void osStopThread(RDRAM_ARG PTR(OSThread) t_) {
assert(false);
}
extern "C" void osDestroyThread(RDRAM_ARG PTR(OSThread) t_) {
if (t_ == NULLPTR) {
t_ = thread_self;
}
OSThread* t = TO_PTR(OSThread, t_);
// Check if the thread is destroying itself (arg is null or thread_self)
if (t_ == thread_self) {
throw ultramodern::thread_terminated{};
}
// Otherwise if the thread isn't stopped, remove it from its currrent queue.,
if (t->state != OSThreadState::STOPPED) {
ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_);
}
// Check if the thread has already been destroyed to prevent destroying it again.
UltraThreadContext* cur_context = t->context;
if (cur_context != nullptr) {
// Mark the target thread as destroyed and resume it. When it starts it'll check this and terminate itself instead of resuming.
t->context = nullptr;
cur_context->running.signal();
}
}
extern "C" void osSetThreadPri(RDRAM_ARG PTR(OSThread) t_, OSPri pri) {
if (t_ == NULLPTR) {
t_ = thread_self;
}
OSThread* t = TO_PTR(OSThread, t_);
if (t->priority != pri) {
t->priority = pri;
if (t_ != ultramodern::this_thread() && t->state != OSThreadState::STOPPED) {
ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_);
ultramodern::thread_queue_insert(PASS_RDRAM t->queue, t_);
}
ultramodern::check_running_queue(PASS_RDRAM1);
}
}
extern "C" OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) t) {
if (t == NULLPTR) {
t = thread_self;
}
return TO_PTR(OSThread, t)->priority;
}
extern "C" OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t) {
if (t == NULLPTR) {
t = thread_self;
}
return TO_PTR(OSThread, t)->id;
}
PTR(OSThread) ultramodern::this_thread() {
return thread_self;
}
static std::thread thread_cleaner_thread;
static moodycamel::BlockingConcurrentQueue<UltraThreadContext*> deleted_threads{};
extern std::atomic_bool exited;
void thread_cleaner_func() {
using namespace std::chrono_literals;
while (!exited) {
UltraThreadContext* to_delete;
if (deleted_threads.wait_dequeue_timed(to_delete, 10ms)) {
debug_printf("[Cleanup] Deleting thread context %p\n", to_delete);
to_delete->host_thread.join();
delete to_delete;
}
}
}
void ultramodern::init_thread_cleanup() {
thread_cleaner_thread = std::thread{thread_cleaner_func};
}
void ultramodern::cleanup_thread(UltraThreadContext *cur_context) {
deleted_threads.enqueue(cur_context);
}
void ultramodern::join_thread_cleaner_thread() {
thread_cleaner_thread.join();
}

View File

@ -1,224 +0,0 @@
#include <thread>
#include <variant>
#include <set>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include "Windows.h"
#endif
// Start time for the program
static std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now();
// Game speed multiplier (1 means no speedup)
constexpr uint32_t speed_multiplier = 1;
// N64 CPU counter ticks per millisecond
constexpr uint32_t counter_per_ms = 46'875 * speed_multiplier;
struct OSTimer {
PTR(OSTimer) unused1;
PTR(OSTimer) unused2;
OSTime interval;
OSTime timestamp;
PTR(OSMesgQueue) mq;
OSMesg msg;
};
struct AddTimerAction {
PTR(OSTimer) timer;
};
struct RemoveTimerAction {
PTR(OSTimer) timer;
};
using Action = std::variant<AddTimerAction, RemoveTimerAction>;
struct {
std::thread thread;
moodycamel::BlockingConcurrentQueue<Action> action_queue{};
} timer_context;
uint64_t duration_to_ticks(std::chrono::high_resolution_clock::duration duration) {
uint64_t delta_micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
// More accurate than using a floating point timer, will only overflow after running for 12.47 years
// Units: (micros * (counts/millis)) / (micros/millis) = counts
uint64_t total_count = (delta_micros * counter_per_ms) / 1000;
return total_count;
}
std::chrono::microseconds ticks_to_duration(uint64_t ticks) {
using namespace std::chrono_literals;
return ticks * 1000us / counter_per_ms;
}
std::chrono::high_resolution_clock::time_point ticks_to_timepoint(uint64_t ticks) {
return start_time + ticks_to_duration(ticks);
}
uint64_t time_now() {
return duration_to_ticks(std::chrono::high_resolution_clock::now() - start_time);
}
void timer_thread(RDRAM_ARG1) {
ultramodern::set_native_thread_name("Timer Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::VeryHigh);
// Lambda comparator function to keep the set ordered
auto timer_sort = [PASS_RDRAM1](PTR(OSTimer) a_, PTR(OSTimer) b_) {
OSTimer* a = TO_PTR(OSTimer, a_);
OSTimer* b = TO_PTR(OSTimer, b_);
// Order by timestamp if the timers have different timestamps
if (a->timestamp != b->timestamp) {
return a->timestamp < b->timestamp;
}
// If they have the exact same timestamp then order by address instead
return a < b;
};
// Ordered set of timers that are currently active
std::set<PTR(OSTimer), decltype(timer_sort)> active_timers{timer_sort};
// Lambda to process a timer action to handle adding and removing timers
auto process_timer_action = [&](const Action& action) {
// Determine the action type and act on it
if (const auto* add_action = std::get_if<AddTimerAction>(&action)) {
active_timers.insert(add_action->timer);
} else if (const auto* remove_action = std::get_if<RemoveTimerAction>(&action)) {
active_timers.erase(remove_action->timer);
}
};
while (true) {
// Empty the action queue
Action cur_action;
while (timer_context.action_queue.try_dequeue(cur_action)) {
process_timer_action(cur_action);
}
// If there's no timer to act on, wait for one to come in from the action queue
while (active_timers.empty()) {
timer_context.action_queue.wait_dequeue(cur_action);
process_timer_action(cur_action);
}
// Get the timer that's closest to running out
PTR(OSTimer) cur_timer_ = *active_timers.begin();
OSTimer* cur_timer = TO_PTR(OSTimer, cur_timer_);
// Remove the timer from the queue (it may get readded if waiting is interrupted)
active_timers.erase(cur_timer_);
// Determine how long to wait to reach the timer's timestamp
auto wait_duration = ticks_to_timepoint(cur_timer->timestamp) - std::chrono::high_resolution_clock::now();
// Wait for either the duration to complete or a new action to come through
if (wait_duration.count() >= 0 && timer_context.action_queue.wait_dequeue_timed(cur_action, wait_duration)) {
// Timer was interrupted by a new action
// Add the current timer back to the queue (done first in case the action is to remove this timer)
active_timers.insert(cur_timer_);
// Process the new action
process_timer_action(cur_action);
}
else {
// Waiting for the timer completed, so send the timer's message to its message queue
osSendMesg(PASS_RDRAM cur_timer->mq, cur_timer->msg, OS_MESG_NOBLOCK);
// If the timer has a specified interval then reload it with that value
if (cur_timer->interval != 0) {
cur_timer->timestamp = cur_timer->interval + time_now();
active_timers.insert(cur_timer_);
}
}
}
}
void ultramodern::init_timers(RDRAM_ARG1) {
timer_context.thread = std::thread{ timer_thread, PASS_RDRAM1 };
timer_context.thread.detach();
}
uint32_t ultramodern::get_speed_multiplier() {
return speed_multiplier;
}
std::chrono::high_resolution_clock::time_point ultramodern::get_start() {
return start_time;
}
std::chrono::high_resolution_clock::duration ultramodern::time_since_start() {
return std::chrono::high_resolution_clock::now() - start_time;
}
extern "C" u32 osGetCount() {
uint64_t total_count = time_now();
// Allow for overflows, which is how osGetCount behaves
return (uint32_t)total_count;
}
extern "C" OSTime osGetTime() {
uint64_t total_count = time_now();
return total_count;
}
extern "C" int osSetTimer(RDRAM_ARG PTR(OSTimer) t_, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg) {
OSTimer* t = TO_PTR(OSTimer, t_);
// Determine the time when this timer will trigger off
if (countdown == 0) {
// Set the timestamp based on the interval
t->timestamp = interval + time_now();
} else {
t->timestamp = countdown + time_now();
}
t->interval = interval;
t->mq = mq;
t->msg = msg;
timer_context.action_queue.enqueue(AddTimerAction{ t_ });
return 0;
}
extern "C" int osStopTimer(RDRAM_ARG PTR(OSTimer) t_) {
timer_context.action_queue.enqueue(RemoveTimerAction{ t_ });
// TODO don't blindly return 0 here; requires some response from the timer thread to know what the returned value was
return 0;
}
#ifdef _WIN32
// The implementations of std::chrono::sleep_until and sleep_for were affected by changing the system clock backwards in older versions
// of Microsoft's STL. This was fixed as of Visual Studio 2022 17.9, but to be safe ultramodern uses Win32 Sleep directly.
void ultramodern::sleep_milliseconds(uint32_t millis) {
Sleep(millis);
}
void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) {
auto time_now = std::chrono::high_resolution_clock::now();
if (time_point > time_now) {
long long delta_ms = std::chrono::ceil<std::chrono::milliseconds>(time_point - time_now).count();
// printf("Sleeping %lld %d ms\n", delta_ms, (uint32_t)delta_ms);
Sleep(delta_ms);
}
}
#else
void ultramodern::sleep_milliseconds(uint32_t millis) {
std::this_thread::sleep_for(std::chrono::milliseconds{millis});
}
void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) {
std::this_thread::sleep_until(time_point);
}
#endif

View File

@ -1,260 +0,0 @@
#ifndef __ULTRA64_ultramodern_H__
#define __ULTRA64_ultramodern_H__
#include <stdint.h>
#ifdef __GNUC__
#define UNUSED __attribute__((unused))
#define ALIGNED(x) __attribute__((aligned(x)))
#else
#define UNUSED
#define ALIGNED(x)
#endif
typedef int64_t s64;
typedef uint64_t u64;
typedef int32_t s32;
typedef uint32_t u32;
typedef int16_t s16;
typedef uint16_t u16;
typedef int8_t s8;
typedef uint8_t u8;
#if 0 // For native compilation
# define PTR(x) x*
# define RDRAM_ARG
# define RDRAM_ARG1
# define PASS_RDRAM
# define PASS_RDRAM1
# define TO_PTR(type, var) var
# define GET_MEMBER(type, addr, member) (&addr->member)
# ifdef __cplusplus
# define NULLPTR nullptr
# endif
#else
# define PTR(x) int32_t
# define RDRAM_ARG uint8_t *rdram,
# define RDRAM_ARG1 uint8_t *rdram
# define PASS_RDRAM rdram,
# define PASS_RDRAM1 rdram
# define TO_PTR(type, var) ((type*)(&rdram[(uint64_t)var - 0xFFFFFFFF80000000]))
# define GET_MEMBER(type, addr, member) (addr + (intptr_t)&(((type*)nullptr)->member))
# ifdef __cplusplus
# define NULLPTR (PTR(void))0
# endif
#endif
#ifndef NULL
#define NULL (PTR(void) 0)
#endif
#define OS_MESG_NOBLOCK 0
#define OS_MESG_BLOCK 1
typedef s32 OSPri;
typedef s32 OSId;
typedef u64 OSTime;
#define OS_EVENT_SW1 0 /* CPU SW1 interrupt */
#define OS_EVENT_SW2 1 /* CPU SW2 interrupt */
#define OS_EVENT_CART 2 /* Cartridge interrupt: used by rmon */
#define OS_EVENT_COUNTER 3 /* Counter int: used by VI/Timer Mgr */
#define OS_EVENT_SP 4 /* SP task done interrupt */
#define OS_EVENT_SI 5 /* SI (controller) interrupt */
#define OS_EVENT_AI 6 /* AI interrupt */
#define OS_EVENT_VI 7 /* VI interrupt: used by VI/Timer Mgr */
#define OS_EVENT_PI 8 /* PI interrupt: used by PI Manager */
#define OS_EVENT_DP 9 /* DP full sync interrupt */
#define OS_EVENT_CPU_BREAK 10 /* CPU breakpoint: used by rmon */
#define OS_EVENT_SP_BREAK 11 /* SP breakpoint: used by rmon */
#define OS_EVENT_FAULT 12 /* CPU fault event: used by rmon */
#define OS_EVENT_THREADSTATUS 13 /* CPU thread status: used by rmon */
#define OS_EVENT_PRENMI 14 /* Pre NMI interrupt */
#define M_GFXTASK 1
#define M_AUDTASK 2
#define M_VIDTASK 3
#define M_NJPEGTASK 4
/////////////
// Structs //
/////////////
// Threads
typedef struct UltraThreadContext UltraThreadContext;
typedef enum {
STOPPED,
QUEUED,
RUNNING,
BLOCKED
} OSThreadState;
typedef struct OSThread_t {
PTR(struct OSThread_t) next; // Next thread in the given queue
OSPri priority;
PTR(PTR(struct OSThread_t)) queue; // Queue this thread is in, if any
uint32_t pad2;
uint16_t flags; // These two are swapped to reflect rdram byteswapping
uint16_t state;
OSId id;
int32_t pad3;
UltraThreadContext* context; // An actual pointer regardless of platform
int32_t sp;
} OSThread;
typedef u32 OSEvent;
typedef PTR(void) OSMesg;
typedef struct OSMesgQueue {
PTR(OSThread) blocked_on_recv; /* Linked list of threads blocked on receiving from this queue */
PTR(OSThread) blocked_on_send; /* Linked list of threads blocked on sending to this queue */
s32 validCount; /* Number of messages in the queue */
s32 first; /* Index of the first message in the ring buffer */
s32 msgCount; /* Size of message buffer */
PTR(OSMesg) msg; /* Pointer to circular buffer to store messages */
} OSMesgQueue;
// RSP
typedef struct {
u32 type;
u32 flags;
PTR(u64) ucode_boot;
u32 ucode_boot_size;
PTR(u64) ucode;
u32 ucode_size;
PTR(u64) ucode_data;
u32 ucode_data_size;
PTR(u64) dram_stack;
u32 dram_stack_size;
PTR(u64) output_buff;
PTR(u64) output_buff_size;
PTR(u64) data_ptr;
u32 data_size;
PTR(u64) yield_data_ptr;
u32 yield_data_size;
} OSTask_s;
typedef union {
OSTask_s t;
int64_t force_structure_alignment;
} OSTask;
// PI
struct OSIoMesgHdr {
// These 3 reversed due to endianness
u8 status; /* Return status */
u8 pri; /* Message priority (High or Normal) */
u16 type; /* Message type */
PTR(OSMesgQueue) retQueue; /* Return message queue to notify I/O completion */
};
struct OSIoMesg {
OSIoMesgHdr hdr; /* Message header */
PTR(void) dramAddr; /* RDRAM buffer address (DMA) */
u32 devAddr; /* Device buffer address (DMA) */
u32 size; /* DMA transfer size in bytes */
u32 piHandle; /* PI device handle */
};
struct OSPiHandle {
PTR(OSPiHandle_s) unused; /* point to next handle on the table */
// These four members reversed due to endianness
u8 relDuration; /* domain release duration */
u8 pageSize; /* domain page size */
u8 latency; /* domain latency */
u8 type; /* DEVICE_TYPE_BULK for disk */
// These three members reversed due to endianness
u16 padding; /* struct alignment padding */
u8 domain; /* which domain */
u8 pulse; /* domain pulse width */
u32 baseAddress; /* Domain address */
u32 speed; /* for roms only */
/* The following are "private" elements" */
u32 transferInfo[18]; /* for disk only */
};
typedef struct {
u32 ctrl;
u32 width;
u32 burst;
u32 vSync;
u32 hSync;
u32 leap;
u32 hStart;
u32 xScale;
u32 vCurrent;
} OSViCommonRegs;
typedef struct {
u32 origin;
u32 yScale;
u32 vStart;
u32 vBurst;
u32 vIntr;
} OSViFieldRegs;
typedef struct {
u8 padding[3];
u8 type;
OSViCommonRegs comRegs;
OSViFieldRegs fldRegs[2];
} OSViMode;
///////////////
// Functions //
///////////////
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
void osInitialize(void);
typedef void (thread_func_t)(PTR(void));
void osCreateThread(RDRAM_ARG PTR(OSThread) t, OSId id, PTR(thread_func_t) entry, PTR(void) arg, PTR(void) sp, OSPri p);
void osStartThread(RDRAM_ARG PTR(OSThread) t);
void osStopThread(RDRAM_ARG PTR(OSThread) t);
void osDestroyThread(RDRAM_ARG PTR(OSThread) t);
void osYieldThread(RDRAM_ARG1);
void osSetThreadPri(RDRAM_ARG PTR(OSThread) t, OSPri pri);
OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) thread);
OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t);
void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32);
s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32);
s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32);
s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32);
void osSetEventMesg(RDRAM_ARG OSEvent, PTR(OSMesgQueue), OSMesg);
void osViSetEvent(RDRAM_ARG PTR(OSMesgQueue), OSMesg, u32);
void osViSwapBuffer(RDRAM_ARG PTR(void) frameBufPtr);
void osViSetMode(RDRAM_ARG PTR(OSViMode));
void osViSetSpecialFeatures(uint32_t func);
void osViBlack(uint8_t active);
void osViSetXScale(float scale);
void osViSetYScale(float scale);
PTR(void) osViGetNextFramebuffer();
PTR(void) osViGetCurrentFramebuffer();
u32 osGetCount();
OSTime osGetTime();
int osSetTimer(RDRAM_ARG PTR(OSTimer) timer, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg);
int osStopTimer(RDRAM_ARG PTR(OSTimer) timer);
u32 osVirtualToPhysical(PTR(void) addr);
#ifdef __cplusplus
} // extern "C"
#endif
#endif

View File

@ -1,14 +0,0 @@
#include "ultra64.h"
#include "ultramodern.hpp"
void ultramodern::preinit(RDRAM_ARG ultramodern::WindowHandle window_handle) {
ultramodern::set_main_thread();
ultramodern::init_events(PASS_RDRAM window_handle);
ultramodern::init_timers(PASS_RDRAM1);
ultramodern::init_audio();
ultramodern::init_saving(PASS_RDRAM1);
ultramodern::init_thread_cleanup();
}
extern "C" void osInitialize() {
}

View File

@ -1,166 +0,0 @@
#ifndef __ultramodern_HPP__
#define __ultramodern_HPP__
#include <thread>
#include <cassert>
#include <stdexcept>
#include <span>
#undef MOODYCAMEL_DELETE_FUNCTION
#define MOODYCAMEL_DELETE_FUNCTION = delete
#include "lightweightsemaphore.h"
#include "ultra64.h"
#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# include <Windows.h>
#elif defined(__ANDROID__)
# include "android/native_window.h"
#elif defined(__linux__)
# include "X11/Xlib.h"
# undef None
# undef Status
# undef LockMask
# undef Always
# undef Success
#endif
struct UltraThreadContext {
std::thread host_thread;
moodycamel::LightweightSemaphore running;
moodycamel::LightweightSemaphore initialized;
};
namespace ultramodern {
#if defined(_WIN32)
// Native HWND handle to the target window.
struct WindowHandle {
HWND window;
DWORD thread_id = (DWORD)-1;
auto operator<=>(const WindowHandle&) const = default;
};
#elif defined(__ANDROID__)
using WindowHandle = ANativeWindow*;
#elif defined(__linux__)
struct WindowHandle {
Display* display;
Window window;
auto operator<=>(const WindowHandle&) const = default;
};
#endif
// We need a place in rdram to hold the PI handles, so pick an address in extended rdram
constexpr uint32_t rdram_size = 1024 * 1024 * 16; // 16MB to give extra room for anything custom
constexpr int32_t cart_handle = 0x80800000;
constexpr int32_t drive_handle = (int32_t)(cart_handle + sizeof(OSPiHandle));
constexpr int32_t flash_handle = (int32_t)(drive_handle + sizeof(OSPiHandle));
constexpr uint32_t save_size = 1024 * 1024 / 8; // Maximum save size, 1Mbit for flash
// Initialization.
void preinit(RDRAM_ARG WindowHandle window_handle);
void init_saving(RDRAM_ARG1);
void init_events(RDRAM_ARG WindowHandle window_handle);
void init_timers(RDRAM_ARG1);
void init_thread_cleanup();
// Thread queues.
constexpr PTR(PTR(OSThread)) running_queue = (PTR(PTR(OSThread)))-1;
void thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue, PTR(OSThread) toadd);
PTR(OSThread) thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue);
bool thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_);
bool thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue);
PTR(OSThread) thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue);
// Message queues.
void wait_for_external_message(RDRAM_ARG1);
// Thread scheduling.
void check_running_queue(RDRAM_ARG1);
void run_next_thread_and_wait(RDRAM_ARG1);
void resume_thread_and_wait(RDRAM_ARG OSThread* t);
void schedule_running_thread(RDRAM_ARG PTR(OSThread) t);
void cleanup_thread(UltraThreadContext* thread_context);
uint32_t permanent_thread_count();
uint32_t temporary_thread_count();
struct thread_terminated : std::exception {};
enum class ThreadPriority {
Low,
Normal,
High,
VeryHigh,
Critical
};
void set_native_thread_name(const std::string& name);
void set_native_thread_priority(ThreadPriority pri);
PTR(OSThread) this_thread();
void set_main_thread();
bool is_game_thread();
void submit_rsp_task(RDRAM_ARG PTR(OSTask) task);
void send_si_message(RDRAM_ARG1);
uint32_t get_speed_multiplier();
// Time
std::chrono::high_resolution_clock::time_point get_start();
std::chrono::high_resolution_clock::duration time_since_start();
void measure_input_latency();
void sleep_milliseconds(uint32_t millis);
void sleep_until(const std::chrono::high_resolution_clock::time_point& time_point);
// Graphics
uint32_t get_target_framerate(uint32_t original);
uint32_t get_display_refresh_rate();
float get_resolution_scale();
void load_shader_cache(std::span<const char> cache_data);
// Audio
void init_audio();
void set_audio_frequency(uint32_t freq);
void queue_audio_buffer(RDRAM_ARG PTR(s16) audio_data, uint32_t byte_count);
uint32_t get_remaining_audio_bytes();
struct audio_callbacks_t {
using queue_samples_t = void(int16_t*, size_t);
using get_samples_remaining_t = size_t();
using set_frequency_t = void(uint32_t);
queue_samples_t* queue_samples;
get_samples_remaining_t* get_frames_remaining;
set_frequency_t* set_frequency;
};
// Input
struct input_callbacks_t {
using poll_input_t = void(void);
using get_input_t = void(uint16_t*, float*, float*);
using set_rumble_t = void(bool);
poll_input_t* poll_input;
get_input_t* get_input;
set_rumble_t* set_rumble;
};
struct gfx_callbacks_t {
using gfx_data_t = void*;
using create_gfx_t = gfx_data_t();
using create_window_t = WindowHandle(gfx_data_t);
using update_gfx_t = void(gfx_data_t);
create_gfx_t* create_gfx;
create_window_t* create_window;
update_gfx_t* update_gfx;
};
bool is_game_started();
void quit();
void join_event_threads();
void join_thread_cleaner_thread();
void join_saving_thread();
} // namespace ultramodern
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define debug_printf(...)
//#define debug_printf(...) printf(__VA_ARGS__);
#endif