Compare commits

...

79 Commits

Author SHA1 Message Date
e8d3ad4d8b UI: RPC: TSUKIHIME -A piece of blue glass moon- asset image 2024-11-23 13:10:53 -06:00
3b6731a351 infra: Undo packing native libraries into executable. 2024-11-22 17:51:44 -06:00
e653848a2c JIT Sparse Function Table (#250)
More up to date build of the JIT Sparse PR for continued development.
JIT Sparse Function Table was originally developed by riperiperi for the
original Ryujinx project, and decreased the amount of layers in the
Function Table structure, to decrease lookup times at the cost of
slightly higher RAM usage.
This PR rebalances the JIT Sparse Function Table to be a bit more RAM
intensive, but faster in workloads where the JIT Function Table is a
bottleneck. Faster RAM will see a bigger impact and slower RAM (DDR3 and
potentially slow DDR4) will see a slight performance decrease.
This PR also implements a base for a PPTC profile system that could
allow for PPTC with ExeFS mods enabled in the future.
This PR also potentially fixes a strange issue where Avalonia would time
out in some rare instances, e.g. when running ExeFS mods with TotK and a
strange controller configuration.

---------

Co-authored-by: Evan Husted <gr33m11@gmail.com>
2024-11-22 15:33:44 -06:00
5534001152 UI: Always save screenshots to the Ryujinx data directory. 2024-11-22 15:08:24 -06:00
e05875a079 UI: It's called "live testing." 2024-11-22 14:52:56 -06:00
49eeb26b6f UI: I may be stupid. Primary button result is Ok, not Yes. 2024-11-22 14:46:10 -06:00
f8d63f9a2f UI: Add a show changelog button in the Updater, for new updates & when you're on the latest version. 2024-11-22 14:38:58 -06:00
e2b7738465 Add all the missing locales from XCI Trimmer and LDN merge (#281)
Hello any fellow developers that may be reading this. Whenever you add
any new locales to `en_US.json`, please make sure to add them to the
rest of the locale files. I will not always be there to add them myself.
2024-11-22 11:07:47 -06:00
1d42c29335 Add more mentions of canary (#258)
This should hopefully make it clearer whether or not you're using
canary.

Changelog:
- Changed github workflows to have "canary" in the zip files
- Added `App.FullAppName` in the about section, so that it's clear in
there too
- Changed log name for canary builds to
`Ryujinx_Canary_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log`
(normal builds should still be
"Ryujinx_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log)
2024-11-21 12:34:53 -06:00
c2de5cc700 Fix really obvious typo, lol 2024-11-21 10:16:13 -06:00
aaaf60b7a4 Change headless to nogui in the release artifacts (#285)
This makes it so that instead of the files you download being
`sdl2-ryujinx-headless` they are now `nogui-ryujinx`in the release (and
canary) artifacts
2024-11-20 12:20:38 -06:00
150e06e0de Add documentation and ldn labels to labeler.yml (#282)
This should make it so that any changes made to ldn and documentation
related files should be auto-labeled
2024-11-20 11:52:16 -06:00
c0a4d95c5d ARMeilleure: Implement TPIDR2_EL0 (#280)
This is an implementation of the TPIDR2_EL0 register. There may be more
potential use-cases for this register not included in this PR, but this
implements the use-case seen in SuperTuxKart.
2024-11-19 13:02:24 -06:00
c3831428e0 Try and fix weird nullref 2024-11-19 10:22:11 -06:00
fda79efed4 Fix Windows builds not being uploaded 2024-11-19 09:31:22 -06:00
8444e4dca0 Fixed mime types button not updating after install/uninstall (#241) 2024-11-19 02:16:15 -06:00
008d908c5a Update Chinese locale missing line (#259) 2024-11-19 01:59:56 -06:00
722953211d Add Zelda Echoes of Wisdom Amiibos informations (#262)
This adds missing informations about Zelda Echoes of Wisdom Amiibos.
2024-11-19 01:59:00 -06:00
df5002bdbf Created bool to store if the "Avilable Update" should be hidden on startup (--hide-updates) (#272)
fixes #263
2024-11-19 01:52:51 -06:00
f4b757c584 Add XCITrimmerTrim and XCITrimmerUntrim Locales (#273) 2024-11-18 15:05:00 -06:00
25d69079cb Fix a couple dead links and spotty wording in docs (#260)
Made it clearer that building is for contributors only in `COMPILING.md`

Fixed 2 dead links in `CONTRIBUTING.md`, that were caused by separating
`COMPILING.md` and file structure changed to `pr-guide.md`
2024-11-17 04:35:37 -06:00
2e1ede5348 Merge remote-tracking branch 'origin/master' 2024-11-17 00:58:10 -06:00
52f42d450f try 1: Fix IndexOutOfBounds in SDL2GamepadDriver.cs 2024-11-17 00:57:56 -06:00
11416e2167 i18n: es_ES: Added missing translations and minor fixes (#242)
Added missing translations and fixed a few spelling mistakes.
2024-11-17 00:17:56 -06:00
e5d076a1b2 Fixed some broken urls (#249) 2024-11-17 00:16:05 -06:00
d394dd769a Updated IT translation file (#243) 2024-11-17 00:15:06 -06:00
6de3afc43d misc: chore: Move build instructions into its own markdown file; remove compatibility section since there's no games list. 2024-11-15 06:02:26 -06:00
9b90e81817 Fix window sizing when "Show Title Bar" is enabled (#247)
Fixes a bug that causes the main window to not size properly when the
TitleBar is enabled (i.e.: when the TitleBar and MenuStrip are separate
entities). Corrects the size for main window startup and when a user
clicks a "View > Window Size > *Resolution Here*" MenuStripItem

Prior to this fix if a user selects 720p/1080p and "Show Title Bar" is
enabled, the window would be sized smaller than intended and display
black bars on the sides of the render area
2024-11-15 01:26:35 -06:00
1e53a17041 misc: Add LEGO Horizon Adventures image asset to Discord RPC 2024-11-15 01:18:00 -06:00
0c23104792 Add mention of canary to README.md (#236) 2024-11-15 00:24:18 -06:00
1ed2aea029 Update ko_KR again 2024-11-14 02:28:00 -06:00
34caa03385 Update ko_KR.json 2024-11-14 02:16:54 -06:00
104701e80d Updtate Korean translation! (#226)
I participated in the Ryujinx Korean localisation through crowdin, but
there were some parts that I couldn't express in my own colours because
of the existing translation, but I started from scratch and coloured it
with my own colours.

There were some duplicates while editing, so I fixed them all.
2024-11-14 02:08:56 -06:00
cef88febb2 Implement IAllSystemAppletProxiesService: 350 (OpenSystemApplicationProxy) (#237)
Implements IAllSystemAppletProxiesService: 350
(OpenSystemApplicationProxy)

This fixes a crash that occurs when launching an NSP forwarder generated
by Nro2Nsp.
2024-11-13 22:29:00 -06:00
5fccfb76b9 Fix divide by zero when recovering from missed draw (Vulkan), authored by EmulationEnjoyer (#235)
Adds the fix for the crash in the opening cutscene of Baldo: The Sacred
Owls when using Vulkan, from ryujinx-mirror. The original discussion
about the fix can be found
[here.](https://github.com/ryujinx-mirror/ryujinx/pull/52)

It's up to you if you want to merge this, it's one of the very few
improvements that ryujinx-mirror got that hasn't made it into your fork
yet. My opinion is that without a graphics expert on board, we can't
know the real cause of this divide-by-zero issue and will have to make
do with this patch to fix it. And I think we will have to do this many
times in the future for other games that suffer crashes at the moment as
well, at least going by current discussions in the #development section
of the discord.

I did not come up with this fix, all credit goes to
[EmulationEnjoyer](https://github.com/EmulationEnjoyer) for putting
Ryujinx through a debugger and discovering the cause of the crash.
2024-11-13 20:36:59 -06:00
4cb5946be4 UI: RPC: Only show hours at maximum for play time 2024-11-11 18:22:19 -06:00
e1dfb48e23 misc: Specify Normal or Canary in Version log line 2024-11-11 18:22:19 -06:00
6d8738c048 TESTERS WANTED: RyuLDN implementation (#65)
These changes allow players to matchmake for local wireless using a LDN
server. The network implementation originates from Berry's public TCP
RyuLDN fork. Logo and unrelated changes have been removed.

Additionally displays LDN game status in the game selection window when
RyuLDN is enabled.

Functionality is only enabled while network mode is set to "RyuLDN" in
the settings.
2024-11-11 16:06:50 -06:00
abfcfcaf0f Fix issue with Logger.Trace (#228) 2024-11-11 14:14:29 -06:00
d404a8b05b i may be stupid 2024-11-10 23:34:30 -06:00
42cbe24bb1 Actually fix Canary showing the wrong repo 2024-11-10 23:32:37 -06:00
79ba9d1258 UI: RPC: Fix asset image hover string version pointing to the Canary repo in Canary 2024-11-10 23:26:15 -06:00
826ffd4a04 misc: Move the LowPowerPtc event handler that changes Optimizations.LowPower into the ConfigurationState ctor 2024-11-10 22:31:26 -06:00
7369079459 misc: chore: cleanup 2 accidental xml namespaces 2024-11-10 22:23:03 -06:00
a506d81989 Split ConfigurationState into 3 parts:
Migration, Model, and everything else.
2024-11-10 22:16:45 -06:00
15c20920b3 I thought this was a typo on my part; it wasn't 2024-11-10 22:04:55 -06:00
285ee276b6 misc: Bake in value change logging into ReactiveObject to reduce logic duplication. 2024-11-10 22:03:12 -06:00
617b81e209 UI: Conditionally enable install/uninstall file types buttons based on whether they're installed already 2024-11-10 20:33:49 -06:00
eb6ce7bcb3 misc: chore: replace some new "" additions & some I missed 2024-11-10 20:09:02 -06:00
69f75f2df1 misc: Fix small code formatting & styling issues 2024-11-10 19:58:02 -06:00
10c8d73b60 UI: Ryujinx Canary title in NCA extractor 2024-11-10 19:10:02 -06:00
e01a30016e RPC: Add Mario & Luigi Brothership image. 2024-11-10 17:01:47 -06:00
e26625dfd5 UI: Disable XCI trimmer button when in-game 2024-11-10 16:17:36 -06:00
9c82d98ec4 headless: Add Ignore Controller Applet as a configurable option 2024-11-10 15:48:07 -06:00
4aae82bad1 misc: Small cleanups 2024-11-10 15:34:24 -06:00
299be822c4 UI: fix: when switching players, it would show old config (#122)
When switching between players' gamepads while saving settings, then
returning to the previous player, the settings show the default settings
instead of the actual settings applied
2024-11-09 23:24:17 -06:00
b17e4f79fb Adds the ability to read a amiibo's nickname from the VirtualAmiiboFile (#217)
This feature adds a way to change the Amiibo's nickname inside Smash and
other places where it's used, so it’s not always "Ryujinx." However, I
did not add a GUI or create the Cabinet applet that would allow users to
change this. So you will have to go to system/amiibo and find your
amiibo id to change it.
2024-11-09 21:18:50 -06:00
a7b58df3fe Appimage Round 2 (#73) 2024-11-09 19:30:19 -06:00
8c2d6192ba Add Dummy Applet to Replace NotImplementedException (#216)
Currently, in Ryujinx, if an app attempts to open an unimplemented
applet, it crashes. This change adds a dummy applet to send a dummy
response instead of crashing and logs the applet.
2024-11-09 19:28:12 -06:00
2a23000fed Add Canary release badge & links 2024-11-08 19:54:36 -06:00
ab7d0a2e6d nuget: bump Microsoft.IdentityModel.JsonWebTokens from 8.0.1 to 8.1.2 (#13) 2024-11-07 10:47:40 -06:00
bd2681b2f9 Add missing and update translations for zh-tw (#158)
Simply add back some missing translations and update outdated
translations for zh-tw.
2024-11-07 10:46:40 -06:00
640d7f9e77 Updater: kinda confused how this didn't work? 2024-11-06 19:55:58 -06:00
02e8278438 Merge remote-tracking branch 'origin/master' 2024-11-06 19:46:30 -06:00
6acd86c890 Fix canary updater & checking if current build is canary. 2024-11-06 19:46:20 -06:00
708256ce96 Add just, a whole bunch of games to RPC assets. (#98) 2024-11-06 19:22:40 -06:00
5bf50836e1 i18n: missing comma in en_US locale 2024-11-06 18:24:30 -06:00
730ba44043 misc: Canary-specific naming & other small changes I had that I need to push. 2024-11-06 18:23:27 -06:00
36c374cc7a fix: remove --deep (#188) 2024-11-06 18:18:59 -06:00
75f714488e Add many missing locales to all languages (#160)
* Added many missing locales
2024-11-06 17:57:12 -06:00
4831965404 Add ability to trim and untrim XCI files from the application context menu AND in Bulk (#105) 2024-11-06 17:37:30 -06:00
47b8145809 Fix fullscreen when using 'Show Title Bar' (#150) 2024-11-06 17:36:30 -06:00
20cc21add6 fix minor grammatical issues in en_US.json (#183) 2024-11-06 17:36:02 -06:00
683baec1af OOPSIE!!!!!!!!! 2024-11-06 17:04:20 -06:00
f4957d2a09 Didn't realize you could compare tags and not just releases although that should have been obvious 2024-11-06 17:00:16 -06:00
3e1182af22 Specify what is a canary tag 2024-11-06 16:55:17 -06:00
6664ed1b11 Merge remote-tracking branch 'origin/master' 2024-11-06 16:48:33 -06:00
0c88b9eff7 Canary & Release separation. 2024-11-06 16:48:20 -06:00
8a064bcd7e Update README.md (#187) 2024-11-05 12:34:38 -06:00
214 changed files with 11996 additions and 3046 deletions

8
.github/labeler.yml vendored
View File

@ -33,3 +33,11 @@ kernel:
infra:
- changed-files:
- any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props']
documentation:
- changed-files:
- any-glob-to-any-file: 'docs/**'
ldn:
- changed-files:
- any-glob-to-any-file: 'src/Ryujinx.HLE/HOS/Services/Ldn/**'

View File

@ -74,36 +74,36 @@ jobs:
chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
#- name: Build AppImage
# if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
# run: |
# PLATFORM_NAME="${{ matrix.platform.name }}"
- name: Build AppImage
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
run: |
PLATFORM_NAME="${{ matrix.platform.name }}"
# sudo apt install -y zsync desktop-file-utils appstream
sudo apt install -y zsync desktop-file-utils appstream
# mkdir -p tools
# export PATH="$PATH:$(readlink -f tools)"
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# # Setup appimagetool
# wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
# chmod +x tools/appimagetool
# chmod +x distribution/linux/appimage/build-appimage.sh
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
# if [ "$PLATFORM_NAME" = "linux-x64" ]; then
# ARCH_NAME=x64
# export ARCH=x86_64
# elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
# ARCH_NAME=arm64
# export ARCH=aarch64
# else
# echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
# exit 1
# fi
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
ARCH_NAME=x64
export ARCH=x86_64
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
ARCH_NAME=arm64
export ARCH=aarch64
else
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
exit 1
fi
# export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
# BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
# shell: bash
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
shell: bash
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v4
@ -112,17 +112,17 @@ jobs:
path: publish
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
#- name: Upload Ryujinx (AppImage) artifact
# uses: actions/upload-artifact@v4
# if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
# with:
# name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
# path: publish_appimage
- name: Upload Ryujinx (AppImage) artifact
uses: actions/upload-artifact@v4
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
path: publish_appimage
- name: Upload Ryujinx.Headless.SDL2 artifact
uses: actions/upload-artifact@v4
with:
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
name: nogui-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_sdl2_headless
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
@ -185,6 +185,6 @@ jobs:
- name: Upload Ryujinx.Headless.SDL2 artifact
uses: actions/upload-artifact@v4
with:
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
name: nogui-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
path: "publish_headless/*.tar.gz"
if: github.event_name == 'pull_request'

257
.github/workflows/canary.yml vendored Normal file
View File

@ -0,0 +1,257 @@
name: Canary release job
on:
workflow_dispatch:
inputs: {}
push:
branches: [ master ]
paths-ignore:
- '.github/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
- '*.json'
- '*.config'
- '*.md'
concurrency: release
env:
POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.2"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary"
RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev"
RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO: "Ryujinx"
RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx-Canary"
RELEASE: 1
jobs:
tag:
name: Create tag
runs-on: ubuntu-20.04
steps:
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
shell: bash
- name: Create tag
uses: actions/github-script@v7
with:
script: |
github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/tags/Canary-${{ steps.version_info.outputs.build_version }}',
sha: context.sha
})
- name: Create release
uses: ncipollo/release-action@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
tag: ${{ steps.version_info.outputs.build_version }}
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}"
omitBodyDuringUpdate: true
owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}
repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}
token: ${{ secrets.RELEASE_TOKEN }}
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
run: |
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Create output dir
run: "mkdir release_output"
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'
run: |
pushd publish_ava
rm publish/libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
pushd publish_sdl2_headless
rm publish/libarmeilleure-jitsupport.dylib
7z a ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
shell: bash
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
run: |
pushd publish_ava
rm publish/libarmeilleure-jitsupport.dylib
chmod +x publish/Ryujinx.sh publish/Ryujinx
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
popd
pushd publish_sdl2_headless
rm publish/libarmeilleure-jitsupport.dylib
chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2
tar -czvf ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
popd
shell: bash
#- name: Build AppImage (Linux)
# if: matrix.platform.os == 'ubuntu-latest'
# run: |
# BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
# PLATFORM_NAME="${{ matrix.platform.name }}"
# sudo apt install -y zsync desktop-file-utils appstream
# mkdir -p tools
# export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
# wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
# chmod +x tools/appimagetool
# chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
# if [ "$PLATFORM_NAME" = "linux-x64" ]; then
# ARCH_NAME=x64
# export ARCH=x86_64
# elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
# ARCH_NAME=arm64
# export ARCH=aarch64
# else
# echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
# exit 1
# fi
# export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
# BUILDDIR=publish_ava OUTDIR=publish_ava_appimage distribution/linux/appimage/build-appimage.sh
# Add to release output
# pushd publish_ava_appimage
# mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
# mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
# popd
# shell: bash
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "release_output/*.tar.gz,release_output/*.zip"
#artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*"
tag: ${{ steps.version_info.outputs.build_version }}
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}"
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}
repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}
token: ${{ secrets.RELEASE_TOKEN }}
macos_release:
name: Release MacOS universal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Setup LLVM 15
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
- name: Install rcodesign
run: |
mkdir -p $HOME/.bin
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
- name: Configure for release
run: |
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
- name: Publish macOS Ryujinx.Headless.SDL2
run: |
./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz"
tag: ${{ steps.version_info.outputs.build_version }}
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}"
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}
repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}
token: ${{ secrets.RELEASE_TOKEN }}

View File

@ -38,12 +38,12 @@ jobs:
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request:\n`;
let hidden_headless_artifacts = `\n\n <details><summary>GUI-less (SDL2)</summary>\n`;
let hidden_headless_artifacts = `\n\n <details><summary>GUI-less</summary>\n`;
let hidden_debug_artifacts = `\n\n <details><summary>Only for Developers</summary>\n`;
for (const art of artifacts) {
if(art.name.includes('Debug')) {
hidden_debug_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else if(art.name.includes('sdl2-ryujinx-headless')) {
} else if(art.name.includes('nogui-ryujinx')) {
hidden_headless_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else {
body += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs: {}
push:
branches: [ master ]
branches: [ release ]
paths-ignore:
- '.github/**'
- 'docs/**'
@ -20,7 +20,7 @@ env:
POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.2"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "master"
RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release"
RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev"
RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx"
RELEASE: 1
@ -93,6 +93,7 @@ jobs:
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
@ -101,83 +102,79 @@ jobs:
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained -p:IncludeNativeLibrariesForSelfExtract=true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'
run: |
pushd publish_ava
rm publish/libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
pushd publish
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
pushd publish_sdl2_headless
rm publish/libarmeilleure-jitsupport.dylib
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
shell: bash
- name: Build AppImage (Linux)
if: matrix.platform.os == 'ubuntu-latest'
run: |
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt install -y zsync desktop-file-utils appstream
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
ARCH_NAME=x64
export ARCH=x86_64
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
ARCH_NAME=arm64
export ARCH=aarch64
else
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
exit 1
fi
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
popd
shell: bash
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
run: |
pushd publish_ava
rm publish/libarmeilleure-jitsupport.dylib
chmod +x publish/Ryujinx.sh publish/Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
pushd publish
chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
pushd publish_sdl2_headless
rm publish/libarmeilleure-jitsupport.dylib
chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
chmod +x Ryujinx.sh Ryujinx.Headless.SDL2
tar -czvf ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
shell: bash
#- name: Build AppImage (Linux)
# if: matrix.platform.os == 'ubuntu-latest'
# run: |
# BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
# PLATFORM_NAME="${{ matrix.platform.name }}"
# sudo apt install -y zsync desktop-file-utils appstream
# mkdir -p tools
# export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
# wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
# chmod +x tools/appimagetool
# chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
# if [ "$PLATFORM_NAME" = "linux-x64" ]; then
# ARCH_NAME=x64
# export ARCH=x86_64
# elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
# ARCH_NAME=arm64
# export ARCH=aarch64
# else
# echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
# exit 1
# fi
# export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
# BUILDDIR=publish_ava OUTDIR=publish_ava_appimage distribution/linux/appimage/build-appimage.sh
# Add to release output
# pushd publish_ava_appimage
# mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
# mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
# popd
# shell: bash
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "release_output/*.tar.gz,release_output/*.zip"
#artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*"
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
tag: ${{ steps.version_info.outputs.build_version }}
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}"
omitBodyDuringUpdate: true
@ -228,22 +225,23 @@ jobs:
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
- name: Publish macOS Ryujinx.Headless.SDL2
run: |
./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz"
artifacts: "publish/*.tar.gz, publish_headless/*.tar.gz"
tag: ${{ steps.version_info.outputs.build_version }}
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}"
omitBodyDuringUpdate: true

23
COMPILING.md Normal file
View File

@ -0,0 +1,23 @@
## Compilation
Building the project is for users that want to contribute code only.
If you wish to build the emulator yourself, follow these steps:
### Step 1
Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json).
### Step 2
Either use `git clone https://github.com/GreemDev/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files.
### Step 3
To build Ryujinx, open a command prompt inside the project directory.
You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`.
Then type the following command: `dotnet build -c Release -o build`
the built files will be found in the newly created build directory.
Ryujinx system files are stored in the `Ryujinx` folder.
This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.

View File

@ -74,7 +74,7 @@ We use and recommend the following workflow:
3. In your fork, create a branch off of main (`git checkout -b mybranch`).
- Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork.
4. Make and commit your changes to your branch.
- [Build Instructions](https://github.com/GreemDev/Ryujinx#building) explains how to build and test.
- [Build Instructions](https://github.com/GreemDev/Ryujinx/blob/master/COMPILING.md) explains how to build and test.
- Commit messages should be clear statements of action and intent.
6. Build the repository with your changes.
- Make sure that the builds are clean.
@ -83,7 +83,7 @@ We use and recommend the following workflow:
- State in the description what issue or improvement your change is addressing.
- Check if all the Continuous Integration checks are passing. Refer to [Actions](https://github.com/GreemDev/Ryujinx/actions) to check for outstanding errors.
8. Wait for feedback or approval of your changes from the core development team
- Details about the pull request [review procedure](docs/workflow/ci/pr-guide.md).
- Details about the pull request [review procedure](docs/workflow/pr-guide.md).
9. When the team members have signed off, and all checks are green, your PR will be merged.
- The next official build will automatically include your change.
- You can delete the branch you used for making the change.

View File

@ -22,7 +22,7 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@ -33,11 +33,12 @@
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
<PackageVersion Include="Open.NAT.Core" Version="2.1.0.5" />
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Gommon" Version="2.6.5" />
<PackageVersion Include="Gommon" Version="2.6.6" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
@ -51,4 +52,4 @@
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

View File

@ -14,6 +14,15 @@
<img src="https://img.shields.io/github/v/release/GreemDev/Ryujinx"
alt="Latest Release">
</a>
<br>
<a href="https://github.com/GreemDev/Ryujinx/actions/workflows/canary.yml">
<img src="https://github.com/GreemDev/Ryujinx/actions/workflows/canary.yml/badge.svg"
alt="">
</a>
<a href="https://github.com/GreemDev/Ryujinx-Canary/releases/latest">
<img src="https://img.shields.io/github/v/release/GreemDev/Ryujinx-Canary?label=canary"
alt="Latest Canary Release">
</a>
</h1>
<p align="center">
@ -33,7 +42,7 @@
Guides and documentation can be found on the <a href="https://github.com/GreemDev/Ryujinx/wiki">Wiki tab</a>.
</p>
<p align="center">
If you would like a version more preservative fork of Ryujinx, check out <a href="https://github.com/ryujinx-mirror/ryujinx">ryujinx-mirror</a>.
If you would like a more preservative fork of Ryujinx, check out <a href="https://github.com/ryujinx-mirror/ryujinx">ryujinx-mirror</a>.
</p>
<p align="center">
@ -47,15 +56,6 @@
<img src="https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/docs/shell.png">
</p>
## Compatibility
As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
Anyone is free to submit a new game test or update an existing game test entry;
simply follow the new issue template and testing guidelines, or post as a reply to the applicable game issue.
Use the search function to see if a game has been tested already!
## Usage
To run this emulator, your PC must be equipped with at least 8GiB of RAM;
@ -63,38 +63,21 @@ failing to meet this requirement may result in a poor gameplay experience or une
## Latest build
These builds are compiled automatically for each commit on the master branch.
While we strive to ensure optimal stability and performance prior to pushing an update, our automated builds **may be unstable or completely broken**.
Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
You can find the latest release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
Canary builds are compiled automatically for each commit on the master branch.
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
These canary builds are only recommended for experienced users.
You can find the latest canary release [here](https://github.com/GreemDev/Ryujinx-Canary/releases/latest).
## Documentation
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
## Building
If you wish to build the emulator yourself, follow these steps:
### Step 1
Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json).
### Step 2
Either use `git clone https://github.com/GreemDev/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files.
### Step 3
To build Ryujinx, open a command prompt inside the project directory.
You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`.
Then type the following command: `dotnet build -c Release -o build`
the built files will be found in the newly created build directory.
Ryujinx system files are stored in the `Ryujinx` folder.
This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
## Features
- **Audio**

View File

@ -29,14 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "src\Ryujinx.Audio\Ryujinx.Audio.csproj", "{806ACF6D-90B0-45D0-A1AC-5F220F3B3985}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Packages.props = Directory.Packages.props
Release Script = .github/workflows/release.yml
Build Script = .github/workflows/build.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Memory", "src\Ryujinx.Memory\Ryujinx.Memory.csproj", "{A5E6C691-9E22-4263-8F40-42F002CE66BE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Memory", "src\Ryujinx.Tests.Memory\Ryujinx.Tests.Memory.csproj", "{D1CC5322-7325-4F6B-9625-194B30BE1296}"
@ -89,6 +81,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Packages.props = Directory.Packages.props
.github/workflows/release.yml = .github/workflows/release.yml
.github/workflows/canary.yml = .github/workflows/canary.yml
.github/workflows/build.yml = .github/workflows/build.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View File

@ -707,6 +707,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010300",
@ -3526,6 +3542,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01400000",
@ -4160,6 +4192,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -5848,6 +5896,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -6126,6 +6190,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -8341,6 +8421,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -9020,6 +9116,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000100",
@ -9496,6 +9608,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010000",
@ -9833,6 +9961,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -14667,6 +14811,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01030000",
@ -16119,6 +16279,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01050000",
@ -16717,6 +16893,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01070000",
@ -19745,6 +19937,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01080000",
@ -20503,6 +20711,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010000",
@ -21805,6 +22029,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -22340,6 +22580,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01020100",
@ -22990,6 +23246,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -23440,6 +23712,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -24660,6 +24948,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01410000",
@ -24954,6 +25258,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01060000",
@ -25286,6 +25606,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -29114,6 +29450,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010000",
@ -32512,6 +32864,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -32928,6 +33296,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000100",
@ -34800,6 +35184,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Red Tunic",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01000000",
@ -37569,6 +37969,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010100",
@ -41293,6 +41709,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Black Cat Clothes",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01020100",
@ -45153,6 +45585,22 @@
"0100F2C0115B6000"
],
"gameName": "The Legend of Zelda: Tears of the Kingdom"
},
{
"amiiboUsage": [
{
"Usage": "Receive the Blue Attire",
"write": false
},
{
"Usage": "Receive random materials",
"write": false
}
],
"gameID": [
"01008CF01BAAC000"
],
"gameName": "The Legend of Zelda: Echoes of Wisdom"
}
],
"head": "01010000",
@ -47896,5 +48344,5 @@
"type": "Figure"
}
],
"lastUpdated": "2024-10-01T00:00:25.035619"
}
"lastUpdated": "2024-11-17T15:28:47.035619"
}

View File

@ -46,5 +46,5 @@ then
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
else
echo "Usign codesign for ad-hoc signing"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$APP_BUNDLE_DIRECTORY"
fi
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$APP_BUNDLE_DIRECTORY"
fi

View File

@ -2,8 +2,8 @@
set -e
if [ "$#" -lt 7 ]; then
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION> <EXTRA_ARGS>"
if [ "$#" -lt 8 ]; then
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION> <CANARY>"
exit 1
fi
@ -18,10 +18,11 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
VERSION=$5
SOURCE_REVISION_ID=$6
CONFIGURATION=$7
EXTRA_ARGS=$8
CANARY=$8
if [ "$VERSION" == "1.1.0" ];
then
if [ "$CANARY" == "1" ]; then
RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_universal.app.tar
elif [ "$VERSION" == "1.1.0" ]; then
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
else
RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
@ -61,7 +62,7 @@ mkdir -p "$OUTPUT_DIRECTORY"
cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE"
rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
# Make it libraries universal
# Make its libraries universal
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib"
if ! [ -x "$(command -v lipo)" ];
@ -99,7 +100,7 @@ then
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
else
echo "Using codesign for ad-hoc signing"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$UNIVERSAL_APP_BUNDLE"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE"
fi
echo "Creating archive"
@ -111,4 +112,4 @@ rm "$RELEASE_TAR_FILE_NAME"
popd
echo "Done"
echo "Done"

View File

@ -2,8 +2,8 @@
set -e
if [ "$#" -lt 7 ]; then
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION> <EXTRA_ARGS>"
if [ "$#" -lt 8 ]; then
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION> <CANARY>"
exit 1
fi
@ -18,13 +18,14 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
VERSION=$5
SOURCE_REVISION_ID=$6
CONFIGURATION=$7
EXTRA_ARGS=$8
CANARY=$8
if [ "$VERSION" == "1.1.0" ];
then
RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar
if [ "$CANARY" == "1" ]; then
RELEASE_TAR_FILE_NAME=nogui-ryujinx-canary-$VERSION-macos_universal.tar
elif [ "$VERSION" == "1.1.0" ]; then
RELEASE_TAR_FILE_NAME=nogui-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar
else
RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$VERSION-macos_universal.tar
RELEASE_TAR_FILE_NAME=nogui-ryujinx-$VERSION-macos_universal.tar
fi
ARM64_OUTPUT="$TEMP_DIRECTORY/publish_arm64"
@ -56,7 +57,7 @@ mkdir -p "$OUTPUT_DIRECTORY"
cp -R "$ARM64_OUTPUT/" "$UNIVERSAL_OUTPUT"
rm "$UNIVERSAL_OUTPUT/$EXECUTABLE_SUB_PATH"
# Make it libraries universal
# Make its libraries universal
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTPUT" "$X64_OUTPUT" "$UNIVERSAL_OUTPUT" "**/*.dylib"
if ! [ -x "$(command -v lipo)" ];
@ -95,7 +96,7 @@ else
echo "Using codesign for ad-hoc signing"
for FILE in "$UNIVERSAL_OUTPUT"/*; do
if [[ $(file "$FILE") == *"Mach-O"* ]]; then
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$FILE"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$FILE"
fi
done
fi
@ -108,4 +109,4 @@ gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
rm "$RELEASE_TAR_FILE_NAME"
popd
echo "Done"
echo "Done"

View File

@ -9,7 +9,7 @@ To merge pull requests, you must have write permissions in the repository.
## Quick Code Review Rules
* Do not mix unrelated changes in one pull request. For example, a code style change should never be mixed with a bug fix.
* All changes should follow the existing code style. You can read more about our code style at [docs/coding-guidelines](../coding-guidelines/coding-style.md).
* All changes should follow the existing code style. You can read more about our code style at [docs/coding-style](../coding-guidelines/coding-style.md).
* Adding external dependencies is to be avoided unless not doing so would introduce _significant_ complexity. Any dependency addition should be justified and discussed before merge.
* Use Draft pull requests for changes you are still working on but want early CI loop feedback. When you think your changes are ready for review, [change the status](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) of your pull request.
* Rebase your changes when required or directly requested. Changes should always be commited on top of the upstream branch, not the other way around.

View File

@ -1,252 +0,0 @@
using ARMeilleure.Diagnostics;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a table of guest address to a value.
/// </summary>
/// <typeparam name="TEntry">Type of the value</typeparam>
public unsafe class AddressTable<TEntry> : IDisposable where TEntry : unmanaged
{
/// <summary>
/// Represents a level in an <see cref="AddressTable{TEntry}"/>.
/// </summary>
public readonly struct Level
{
/// <summary>
/// Gets the index of the <see cref="Level"/> in the guest address.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the length of the <see cref="Level"/> in the guest address.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the mask which masks the bits used by the <see cref="Level"/>.
/// </summary>
public ulong Mask => ((1ul << Length) - 1) << Index;
/// <summary>
/// Initializes a new instance of the <see cref="Level"/> structure with the specified
/// <paramref name="index"/> and <paramref name="length"/>.
/// </summary>
/// <param name="index">Index of the <see cref="Level"/></param>
/// <param name="length">Length of the <see cref="Level"/></param>
public Level(int index, int length)
{
(Index, Length) = (index, length);
}
/// <summary>
/// Gets the value of the <see cref="Level"/> from the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Value of the <see cref="Level"/> from the specified guest <paramref name="address"/></returns>
public int GetValue(ulong address)
{
return (int)((address & Mask) >> Index);
}
}
private bool _disposed;
private TEntry** _table;
private readonly List<nint> _pages;
/// <summary>
/// Gets the bits used by the <see cref="Levels"/> of the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public ulong Mask { get; }
/// <summary>
/// Gets the <see cref="Level"/>s used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public Level[] Levels { get; }
/// <summary>
/// Gets or sets the default fill value of newly created leaf pages.
/// </summary>
public TEntry Fill { get; set; }
/// <summary>
/// Gets the base address of the <see cref="EntryTable{TEntry}"/>.
/// </summary>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
public nint Base
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_pages)
{
return (nint)GetRootPage();
}
}
}
/// <summary>
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
/// <see cref="Level"/>.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="levels"/> is null</exception>
/// <exception cref="ArgumentException">Length of <paramref name="levels"/> is less than 2</exception>
public AddressTable(Level[] levels)
{
ArgumentNullException.ThrowIfNull(levels);
if (levels.Length < 2)
{
throw new ArgumentException("Table must be at least 2 levels deep.", nameof(levels));
}
_pages = new List<nint>(capacity: 16);
Levels = levels;
Mask = 0;
foreach (var level in Levels)
{
Mask |= level.Mask;
}
}
/// <summary>
/// Determines if the specified <paramref name="address"/> is in the range of the
/// <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns><see langword="true"/> if is valid; otherwise <see langword="false"/></returns>
public bool IsValid(ulong address)
{
return (address & ~Mask) == 0;
}
/// <summary>
/// Gets a reference to the value at the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Reference to the value at the specified guest <paramref name="address"/></returns>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
/// <exception cref="ArgumentException"><paramref name="address"/> is not mapped</exception>
public ref TEntry GetValue(ulong address)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!IsValid(address))
{
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
}
lock (_pages)
{
return ref GetPage(address)[Levels[^1].GetValue(address)];
}
}
/// <summary>
/// Gets the leaf page for the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Leaf page for the specified guest <paramref name="address"/></returns>
private TEntry* GetPage(ulong address)
{
TEntry** page = GetRootPage();
for (int i = 0; i < Levels.Length - 1; i++)
{
ref Level level = ref Levels[i];
ref TEntry* nextPage = ref page[level.GetValue(address)];
if (nextPage == null)
{
ref Level nextLevel = ref Levels[i + 1];
nextPage = i == Levels.Length - 2 ?
(TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true) :
(TEntry*)Allocate(1 << nextLevel.Length, nint.Zero, leaf: false);
}
page = (TEntry**)nextPage;
}
return (TEntry*)page;
}
/// <summary>
/// Lazily initialize and get the root page of the <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <returns>Root page of the <see cref="AddressTable{TEntry}"/></returns>
private TEntry** GetRootPage()
{
if (_table == null)
{
_table = (TEntry**)Allocate(1 << Levels[0].Length, fill: nint.Zero, leaf: false);
}
return _table;
}
/// <summary>
/// Allocates a block of memory of the specified type and length.
/// </summary>
/// <typeparam name="T">Type of elements</typeparam>
/// <param name="length">Number of elements</param>
/// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns>
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{
var size = sizeof(T) * length;
var page = (nint)NativeAllocator.Instance.Allocate((uint)size);
var span = new Span<T>((void*)page, length);
span.Fill(fill);
_pages.Add(page);
TranslatorEventSource.Log.AddressTableAllocated(size, leaf);
return page;
}
/// <summary>
/// Releases all resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases all unmanaged and optionally managed resources used by the <see cref="AddressTable{TEntry}"/>
/// instance.
/// </summary>
/// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
foreach (var page in _pages)
{
Marshal.FreeHGlobal(page);
}
_disposed = true;
}
}
/// <summary>
/// Frees resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
~AddressTable()
{
Dispose(false);
}
}
}

View File

@ -0,0 +1,44 @@
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a level in an <see cref="IAddressTable{TEntry}"/>.
/// </summary>
public readonly struct AddressTableLevel
{
/// <summary>
/// Gets the index of the <see cref="Level"/> in the guest address.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the length of the <see cref="AddressTableLevel"/> in the guest address.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the mask which masks the bits used by the <see cref="AddressTableLevel"/>.
/// </summary>
public ulong Mask => ((1ul << Length) - 1) << Index;
/// <summary>
/// Initializes a new instance of the <see cref="AddressTableLevel"/> structure with the specified
/// <paramref name="index"/> and <paramref name="length"/>.
/// </summary>
/// <param name="index">Index of the <see cref="AddressTableLevel"/></param>
/// <param name="length">Length of the <see cref="AddressTableLevel"/></param>
public AddressTableLevel(int index, int length)
{
(Index, Length) = (index, length);
}
/// <summary>
/// Gets the value of the <see cref="AddressTableLevel"/> from the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Value of the <see cref="AddressTableLevel"/> from the specified guest <paramref name="address"/></returns>
public int GetValue(ulong address)
{
return (int)((address & Mask) >> Index);
}
}
}

View File

@ -0,0 +1,75 @@
namespace ARMeilleure.Common
{
public static class AddressTablePresets
{
private static readonly AddressTableLevel[] _levels64Bit =
new AddressTableLevel[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTableLevel[] _levels32Bit =
new AddressTableLevel[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private static readonly AddressTableLevel[] _levels64BitSparseTiny =
new AddressTableLevel[]
{
new( 11, 28),
new( 2, 9),
};
private static readonly AddressTableLevel[] _levels32BitSparseTiny =
new AddressTableLevel[]
{
new( 10, 22),
new( 1, 9),
};
private static readonly AddressTableLevel[] _levels64BitSparseGiant =
new AddressTableLevel[]
{
new( 38, 1),
new( 2, 36),
};
private static readonly AddressTableLevel[] _levels32BitSparseGiant =
new AddressTableLevel[]
{
new( 31, 1),
new( 1, 30),
};
//high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization
//low power will never run worse than non-sparse, but for most systems it won't be necessary
//high power is always used, but I've left low power in here for future reference
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false)
{
if (sparse)
{
if (lowPower)
{
return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny;
}
else
{
return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant;
}
}
else
{
return for64Bits ? _levels64Bit : _levels32Bit;
}
}
}
}

View File

@ -2,7 +2,7 @@ using System;
namespace ARMeilleure.Common
{
unsafe abstract class Allocator : IDisposable
public unsafe abstract class Allocator : IDisposable
{
public T* Allocate<T>(ulong count = 1) where T : unmanaged
{

View File

@ -0,0 +1,51 @@
using System;
namespace ARMeilleure.Common
{
public interface IAddressTable<TEntry> : IDisposable where TEntry : unmanaged
{
/// <summary>
/// True if the address table's bottom level is sparsely mapped.
/// This also ensures the second bottom level is filled with a dummy page rather than 0.
/// </summary>
bool Sparse { get; }
/// <summary>
/// Gets the bits used by the <see cref="Levels"/> of the <see cref="IAddressTable{TEntry}"/> instance.
/// </summary>
ulong Mask { get; }
/// <summary>
/// Gets the <see cref="AddressTableLevel"/>s used by the <see cref="IAddressTable{TEntry}"/> instance.
/// </summary>
AddressTableLevel[] Levels { get; }
/// <summary>
/// Gets or sets the default fill value of newly created leaf pages.
/// </summary>
TEntry Fill { get; set; }
/// <summary>
/// Gets the base address of the <see cref="EntryTable{TEntry}"/>.
/// </summary>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
nint Base { get; }
/// <summary>
/// Determines if the specified <paramref name="address"/> is in the range of the
/// <see cref="IAddressTable{TEntry}"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns><see langword="true"/> if is valid; otherwise <see langword="false"/></returns>
bool IsValid(ulong address);
/// <summary>
/// Gets a reference to the value at the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Reference to the value at the specified guest <paramref name="address"/></returns>
/// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
/// <exception cref="ArgumentException"><paramref name="address"/> is not mapped</exception>
ref TEntry GetValue(ulong address);
}
}

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace ARMeilleure.Common
{
unsafe sealed class NativeAllocator : Allocator
public unsafe sealed class NativeAllocator : Allocator
{
public static NativeAllocator Instance { get; } = new();

View File

@ -193,6 +193,8 @@ namespace ARMeilleure.Instructions
Operand hostAddress;
var table = context.FunctionTable;
// If address is mapped onto the function table, we can skip the table walk. Otherwise we fallback
// onto the dispatch stub.
if (guestAddress.Kind == OperandKind.Constant && context.FunctionTable.IsValid(guestAddress.Value))
@ -203,6 +205,30 @@ namespace ARMeilleure.Instructions
hostAddress = context.Load(OperandType.I64, hostAddressAddr);
}
else if (table.Sparse)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
// Deliberately attempts to avoid branches.
Operand tableBase = !context.HasPtc ?
Const(table.Base) :
Const(table.Base, Ptc.FunctionTableSymbol);
hostAddress = tableBase;
for (int i = 0; i < table.Levels.Length; i++)
{
var level = table.Levels[i];
int clearBits = 64 - (level.Index + level.Length);
Operand index = context.ShiftLeft(
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
Const(3)
);
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
}
}
else
{
hostAddress = !context.HasPtc ?

View File

@ -49,6 +49,9 @@ namespace ARMeilleure.Instructions
case 0b11_011_1101_0000_011:
EmitGetTpidrroEl0(context);
return;
case 0b11_011_1101_0000_101:
EmitGetTpidr2El0(context);
return;
case 0b11_011_1110_0000_000:
info = typeof(NativeInterface).GetMethod(nameof(NativeInterface.GetCntfrqEl0));
break;
@ -84,6 +87,9 @@ namespace ARMeilleure.Instructions
case 0b11_011_1101_0000_010:
EmitSetTpidrEl0(context);
return;
case 0b11_011_1101_0000_101:
EmitGetTpidr2El0(context);
return;
default:
throw new NotImplementedException($"Unknown MSR 0x{op.RawOpCode:X8} at 0x{op.Address:X16}.");
@ -213,6 +219,17 @@ namespace ARMeilleure.Instructions
SetIntOrZR(context, op.Rt, result);
}
private static void EmitGetTpidr2El0(ArmEmitterContext context)
{
OpCodeSystem op = (OpCodeSystem)context.CurrOp;
Operand nativeContext = context.LoadArgument(OperandType.I64, 0);
Operand result = context.Load(OperandType.I64, context.Add(nativeContext, Const((ulong)NativeContext.GetTpidr2El0Offset())));
SetIntOrZR(context, op.Rt, result);
}
private static void EmitSetNzcv(ArmEmitterContext context)
{
OpCodeSystem op = (OpCodeSystem)context.CurrOp;

View File

@ -8,7 +8,7 @@ namespace ARMeilleure.Signal
{
public static class NativeSignalHandlerGenerator
{
public const int MaxTrackedRanges = 8;
public const int MaxTrackedRanges = 16;
private const int StructAddressOffset = 0;
private const int StructWriteOffset = 4;

View File

@ -21,6 +21,7 @@ namespace ARMeilleure.State
public ulong ExclusiveValueLow;
public ulong ExclusiveValueHigh;
public int Running;
public long Tpidr2El0;
}
private static NativeCtxStorage _dummyStorage = new();
@ -176,6 +177,9 @@ namespace ARMeilleure.State
public long GetTpidrroEl0() => GetStorage().TpidrroEl0;
public void SetTpidrroEl0(long value) => GetStorage().TpidrroEl0 = value;
public long GetTpidr2El0() => GetStorage().Tpidr2El0;
public void SetTpidr2El0(long value) => GetStorage().Tpidr2El0 = value;
public int GetCounter() => GetStorage().Counter;
public void SetCounter(int value) => GetStorage().Counter = value;
@ -232,6 +236,11 @@ namespace ARMeilleure.State
return StorageOffset(ref _dummyStorage, ref _dummyStorage.TpidrroEl0);
}
public static int GetTpidr2El0Offset()
{
return StorageOffset(ref _dummyStorage, ref _dummyStorage.Tpidr2El0);
}
public static int GetCounterOffset()
{
return StorageOffset(ref _dummyStorage, ref _dummyStorage.Counter);

View File

@ -46,7 +46,7 @@ namespace ARMeilleure.Translation
public IMemoryManager Memory { get; }
public EntryTable<uint> CountTable { get; }
public AddressTable<ulong> FunctionTable { get; }
public IAddressTable<ulong> FunctionTable { get; }
public TranslatorStubs Stubs { get; }
public ulong EntryAddress { get; }
@ -62,7 +62,7 @@ namespace ARMeilleure.Translation
public ArmEmitterContext(
IMemoryManager memory,
EntryTable<uint> countTable,
AddressTable<ulong> funcTable,
IAddressTable<ulong> funcTable,
TranslatorStubs stubs,
ulong entryAddress,
bool highCq,

View File

@ -77,7 +77,7 @@ namespace ARMeilleure.Translation
{
continue;
}
for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++)
{
BasicBlock current = block.Predecessors[pBlkIndex];

View File

@ -13,6 +13,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -29,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 6992; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@ -40,6 +41,7 @@ namespace ARMeilleure.Translation.PTC
public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
public static readonly Symbol FunctionTableSymbol = new(SymbolType.Special, 4);
private const byte FillingByte = 0x00;
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
@ -100,7 +102,7 @@ namespace ARMeilleure.Translation.PTC
Disable();
}
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode)
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector)
{
Wait();
@ -126,6 +128,8 @@ namespace ARMeilleure.Translation.PTC
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
_memoryMode = memoryMode;
Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}");
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);
@ -139,8 +143,8 @@ namespace ARMeilleure.Translation.PTC
Directory.CreateDirectory(workPathBackup);
}
CachePathActual = Path.Combine(workPathActual, DisplayVersion);
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion);
CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector;
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector;
PreLoad();
Profiler.PreLoad();
@ -705,6 +709,10 @@ namespace ARMeilleure.Translation.PTC
{
imm = translator.Stubs.DispatchStub;
}
else if (symbol == FunctionTableSymbol)
{
imm = translator.FunctionTable.Base;
}
if (imm == null)
{
@ -848,18 +856,15 @@ namespace ARMeilleure.Translation.PTC
}
}
List<Thread> threads = new();
for (int i = 0; i < degreeOfParallelism; i++)
{
Thread thread = new(TranslateFuncs)
{
IsBackground = true,
Name = "Ptc.TranslateThread." + i
};
threads.Add(thread);
}
List<Thread> threads = Enumerable.Range(0, degreeOfParallelism)
.Select(idx =>
new Thread(TranslateFuncs)
{
IsBackground = true,
Name = "Ptc.TranslateThread." + idx
}
).ToList();
Stopwatch sw = Stopwatch.StartNew();

View File

@ -22,33 +22,13 @@ namespace ARMeilleure.Translation
{
public class Translator
{
private static readonly AddressTable<ulong>.Level[] _levels64Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTable<ulong>.Level[] _levels32Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private readonly IJitMemoryAllocator _allocator;
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly Ptc _ptc;
internal TranslatorCache<TranslatedFunction> Functions { get; }
internal AddressTable<ulong> FunctionTable { get; }
internal IAddressTable<ulong> FunctionTable { get; }
internal EntryTable<uint> CountTable { get; }
internal TranslatorStubs Stubs { get; }
internal TranslatorQueue Queue { get; }
@ -57,7 +37,7 @@ namespace ARMeilleure.Translation
private Thread[] _backgroundTranslationThreads;
private volatile int _threadCount;
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits)
public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, IAddressTable<ulong> functionTable)
{
_allocator = allocator;
Memory = memory;
@ -72,15 +52,15 @@ namespace ARMeilleure.Translation
CountTable = new EntryTable<uint>();
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
}
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type);
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector);
return _ptc;
}

View File

@ -19,7 +19,7 @@ namespace ARMeilleure.Translation
private bool _disposed;
private readonly AddressTable<ulong> _functionTable;
private readonly IAddressTable<ulong> _functionTable;
private readonly Lazy<nint> _dispatchStub;
private readonly Lazy<DispatcherFunction> _dispatchLoop;
private readonly Lazy<WrapperFunction> _contextWrapper;
@ -86,7 +86,7 @@ namespace ARMeilleure.Translation
/// </summary>
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(AddressTable<ulong> functionTable)
public TranslatorStubs(IAddressTable<ulong> functionTable)
{
ArgumentNullException.ThrowIfNull(functionTable);

View File

@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
public enum MultiplayerMode
{
Disabled,
LdnRyu,
LdnMitm,
}
}

View File

@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
TamperMachine,
UI,
Vic,
XCIFileTrimmer
}
}

View File

@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets
string ILogTarget.Name { get => _target.Name; }
public AsyncLogTargetWrapper(ILogTarget target)
: this(target, -1, AsyncLogTargetOverflowAction.Block)
: this(target, -1)
{ }
public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction)
public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block)
{
_target = target;
_messageQueue = new BlockingCollection<LogEventArgs>(queueLimit);

View File

@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets
}
// Clean up old logs, should only keep 3
FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray();
for (int i = 0; i < files.Length - 2; i++)
{
try
@ -69,9 +69,10 @@ namespace Ryujinx.Common.Logging.Targets
}
string version = ReleaseInformation.Version;
string appName = ReleaseInformation.IsCanaryBuild ? "Ryujinx_Canary" : "Ryujinx";
// Get path for the current time
path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
path = Path.Combine(logDir.FullName, $"{appName}_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
try
{

View File

@ -0,0 +1,30 @@
using Ryujinx.Common.Utilities;
namespace Ryujinx.Common.Logging
{
public class XCIFileTrimmerLog : XCIFileTrimmer.ILog
{
public virtual void Progress(long current, long total, string text, bool complete)
{
}
public void Write(XCIFileTrimmer.LogType logType, string text)
{
switch (logType)
{
case XCIFileTrimmer.LogType.Info:
Logger.Notice.Print(LogClass.XCIFileTrimmer, text);
break;
case XCIFileTrimmer.LogType.Warn:
Logger.Warning?.Print(LogClass.XCIFileTrimmer, text);
break;
case XCIFileTrimmer.LogType.Error:
Logger.Error?.Print(LogClass.XCIFileTrimmer, text);
break;
case XCIFileTrimmer.LogType.Progress:
Logger.Info?.Print(LogClass.XCIFileTrimmer, text);
break;
}
}
}
}

View File

@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array140<T> : IArray<T> where T : unmanaged
{
T _e0;
@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array384<T> : IArray<T> where T : unmanaged
{
T _e0;

View File

@ -1,11 +1,13 @@
using Ryujinx.Common.Logging;
using System;
using System.Globalization;
using System.Threading;
namespace Ryujinx.Common
{
public class ReactiveObject<T>
{
private readonly ReaderWriterLockSlim _readerWriterLock = new();
private readonly ReaderWriterLockSlim _rwLock = new();
private bool _isInitialized;
private T _value;
@ -15,15 +17,15 @@ namespace Ryujinx.Common
{
get
{
_readerWriterLock.EnterReadLock();
_rwLock.EnterReadLock();
T value = _value;
_readerWriterLock.ExitReadLock();
_rwLock.ExitReadLock();
return value;
}
set
{
_readerWriterLock.EnterWriteLock();
_rwLock.EnterWriteLock();
T oldValue = _value;
@ -32,7 +34,7 @@ namespace Ryujinx.Common
_isInitialized = true;
_value = value;
_readerWriterLock.ExitWriteLock();
_rwLock.ExitWriteLock();
if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
{
@ -40,12 +42,22 @@ namespace Ryujinx.Common
}
}
}
public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration)
=> Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName);
public static implicit operator T(ReactiveObject<T> obj) => obj.Value;
}
public static class ReactiveObjectHelper
{
public static void LogValueChange<T>(LogClass logClass, ReactiveEventArgs<T> eventArgs, string valueName)
{
string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}");
Logger.Info?.Print(logClass, message);
}
public static void Toggle(this ReactiveObject<bool> rBoolean) => rBoolean.Value = !rBoolean.Value;
}

View File

@ -1,3 +1,4 @@
using System;
using System.Reflection;
namespace Ryujinx.Common
@ -5,7 +6,9 @@ namespace Ryujinx.Common
// DO NOT EDIT, filled by CI
public static class ReleaseInformation
{
private const string FlatHubChannelOwner = "flathub";
private const string FlatHubChannel = "flathub";
private const string CanaryChannel = "canary";
private const string ReleaseChannel = "release";
private const string BuildVersion = "%%RYUJINX_BUILD_VERSION%%";
public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%";
@ -13,6 +16,7 @@ namespace Ryujinx.Common
private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%";
public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%";
public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%";
public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%";
public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json";
@ -21,11 +25,24 @@ namespace Ryujinx.Common
!BuildGitHash.StartsWith("%%") &&
!ReleaseChannelName.StartsWith("%%") &&
!ReleaseChannelOwner.StartsWith("%%") &&
!ReleaseChannelSourceRepo.StartsWith("%%") &&
!ReleaseChannelRepo.StartsWith("%%") &&
!ConfigFileName.StartsWith("%%");
public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannelOwner);
public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannel);
public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel);
public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel);
public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public static string GetChangelogUrl(Version currentVersion, Version newVersion) =>
IsCanaryBuild
? $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/releases/tag/{newVersion}";
public static string GetChangelogForVersion(Version version) =>
$"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelRepo}/releases/tag/{version}";
}
}

View File

@ -1,6 +1,7 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.Utilities
{
@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities
return (targetProperties, targetAddressInfo);
}
public static bool SupportsDynamicDns()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
public static uint ConvertIpv4Address(IPAddress ipAddress)
{
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());

View File

@ -0,0 +1,524 @@
// Uncomment the line below to ensure XCIFileTrimmer does not modify files
//#define XCI_TRIMMER_READ_ONLY_MODE
using Gommon;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace Ryujinx.Common.Utilities
{
public sealed class XCIFileTrimmer
{
private const long BytesInAMegabyte = 1024 * 1024;
private const int BufferSize = 8 * (int)BytesInAMegabyte;
private const long CartSizeMBinFormattedGB = 952;
private const int CartKeyAreaSize = 0x1000;
private const byte PaddingByte = 0xFF;
private const int HeaderFilePos = 0x100;
private const int CartSizeFilePos = 0x10D;
private const int DataSizeFilePos = 0x118;
private const string HeaderMagicValue = "HEAD";
/// <summary>
/// Cartridge Sizes (ByteIdentifier, SizeInGB)
/// </summary>
private static readonly Dictionary<byte, long> _cartSizesGB = new()
{
{ 0xFA, 1 },
{ 0xF8, 2 },
{ 0xF0, 4 },
{ 0xE0, 8 },
{ 0xE1, 16 },
{ 0xE2, 32 }
};
private static long RecordsToByte(long records)
{
return 512 + (records * 512);
}
public static bool CanTrim(string filename, ILog log = null)
{
if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase))
{
var trimmer = new XCIFileTrimmer(filename, log);
return trimmer.CanBeTrimmed;
}
return false;
}
public static bool CanUntrim(string filename, ILog log = null)
{
if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase))
{
var trimmer = new XCIFileTrimmer(filename, log);
return trimmer.CanBeUntrimmed;
}
return false;
}
private ILog _log;
private string _filename;
private FileStream _fileStream;
private BinaryReader _binaryReader;
private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB;
private bool _fileOK = true;
private bool _freeSpaceChecked = false;
private bool _freeSpaceValid = false;
public enum OperationOutcome
{
Undetermined,
InvalidXCIFile,
NoTrimNecessary,
NoUntrimPossible,
FreeSpaceCheckFailed,
FileIOWriteError,
ReadOnlyFileCannotFix,
FileSizeChanged,
Successful,
Cancelled
}
public enum LogType
{
Info,
Warn,
Error,
Progress
}
public interface ILog
{
public void Write(LogType logType, string text);
public void Progress(long current, long total, string text, bool complete);
}
public bool FileOK => _fileOK;
public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
public bool ContainsKeyArea => _offsetB != 0;
public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB;
public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked;
public bool FreeSpaceValid => _fileOK && _freeSpaceValid;
public long DataSizeB => _dataSizeB;
public long CartSizeB => _cartSizeB;
public long FileSizeB => _fileSizeB;
public long DiskSpaceSavedB => CartSizeB - FileSizeB;
public long DiskSpaceSavingsB => CartSizeB - DataSizeB;
public long TrimmedFileSizeB => _offsetB + _dataSizeB;
public long UntrimmedFileSizeB => _offsetB + _cartSizeB;
public ILog Log
{
get => _log;
set => _log = value;
}
public String Filename
{
get => _filename;
set
{
_filename = value;
Reset();
}
}
public long Pos
{
get => _fileStream.Position;
set => _fileStream.Position = value;
}
public XCIFileTrimmer(string path, ILog log = null)
{
Log = log;
Filename = path;
ReadHeader();
}
public void CheckFreeSpace(CancellationToken? cancelToken = null)
{
if (FreeSpaceChecked)
return;
try
{
if (CanBeTrimmed)
{
_freeSpaceValid = false;
OpenReaders();
try
{
Pos = TrimmedFileSizeB;
bool freeSpaceValid = true;
long readSizeB = FileSizeB - TrimmedFileSizeB;
Stopwatch timedSw = Lambda.Timed(() =>
{
freeSpaceValid = CheckPadding(readSizeB, cancelToken);
});
if (timedSw.Elapsed.TotalSeconds > 0)
{
Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec");
}
if (freeSpaceValid)
Log?.Write(LogType.Info, "Free space is valid");
_freeSpaceValid = freeSpaceValid;
}
finally
{
CloseReaders();
}
}
else
{
Log?.Write(LogType.Warn, "There is no free space to check.");
_freeSpaceValid = false;
}
}
finally
{
_freeSpaceChecked = true;
}
}
private bool CheckPadding(long readSizeB, CancellationToken? cancelToken = null)
{
long maxReads = readSizeB / XCIFileTrimmer.BufferSize;
long read = 0;
var buffer = new byte[BufferSize];
while (true)
{
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
{
return false;
}
int bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize);
if (bytes == 0)
break;
Log?.Progress(read, maxReads, "Verifying file can be trimmed", false);
if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte))
{
Log?.Write(LogType.Warn, "Free space is NOT valid");
return false;
}
read++;
}
return true;
}
private void Reset()
{
_freeSpaceChecked = false;
_freeSpaceValid = false;
ReadHeader();
}
public OperationOutcome Trim(CancellationToken? cancelToken = null)
{
if (!FileOK)
{
return OperationOutcome.InvalidXCIFile;
}
if (!CanBeTrimmed)
{
return OperationOutcome.NoTrimNecessary;
}
if (!FreeSpaceChecked)
{
CheckFreeSpace(cancelToken);
}
if (!FreeSpaceValid)
{
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
{
return OperationOutcome.Cancelled;
}
else
{
return OperationOutcome.FreeSpaceCheckFailed;
}
}
Log?.Write(LogType.Info, "Trimming...");
try
{
var info = new FileInfo(Filename);
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
try
{
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly);
}
catch (Exception e)
{
Log?.Write(LogType.Error, e.ToString());
return OperationOutcome.ReadOnlyFileCannotFix;
}
}
if (info.Length != FileSizeB)
{
Log?.Write(LogType.Error, "File size has changed, cannot safely trim.");
return OperationOutcome.FileSizeChanged;
}
var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write);
try
{
#if !XCI_TRIMMER_READ_ONLY_MODE
outfileStream.SetLength(TrimmedFileSizeB);
#endif
return OperationOutcome.Successful;
}
finally
{
outfileStream.Close();
Reset();
}
}
catch (Exception e)
{
Log?.Write(LogType.Error, e.ToString());
return OperationOutcome.FileIOWriteError;
}
}
public OperationOutcome Untrim(CancellationToken? cancelToken = null)
{
if (!FileOK)
{
return OperationOutcome.InvalidXCIFile;
}
if (!CanBeUntrimmed)
{
return OperationOutcome.NoUntrimPossible;
}
try
{
Log?.Write(LogType.Info, "Untrimming...");
var info = new FileInfo(Filename);
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
try
{
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly);
}
catch (Exception e)
{
Log?.Write(LogType.Error, e.ToString());
return OperationOutcome.ReadOnlyFileCannotFix;
}
}
if (info.Length != FileSizeB)
{
Log?.Write(LogType.Error, "File size has changed, cannot safely untrim.");
return OperationOutcome.FileSizeChanged;
}
var outfileStream = new FileStream(_filename, FileMode.Append, FileAccess.Write, FileShare.Write);
long bytesToWriteB = UntrimmedFileSizeB - FileSizeB;
try
{
Stopwatch timedSw = Lambda.Timed(() =>
{
WritePadding(outfileStream, bytesToWriteB, cancelToken);
});
if (timedSw.Elapsed.TotalSeconds > 0)
{
Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec");
}
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
{
return OperationOutcome.Cancelled;
}
else
{
return OperationOutcome.Successful;
}
}
finally
{
outfileStream.Close();
Reset();
}
}
catch (Exception e)
{
Log?.Write(LogType.Error, e.ToString());
return OperationOutcome.FileIOWriteError;
}
}
private void WritePadding(FileStream outfileStream, long bytesToWriteB, CancellationToken? cancelToken = null)
{
long bytesLeftToWriteB = bytesToWriteB;
long writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize;
int write = 0;
try
{
var buffer = new byte[BufferSize];
Array.Fill<byte>(buffer, XCIFileTrimmer.PaddingByte);
while (bytesLeftToWriteB > 0)
{
if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
{
return;
}
long bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB);
#if !XCI_TRIMMER_READ_ONLY_MODE
outfileStream.Write(buffer, 0, (int)bytesToWrite);
#endif
bytesLeftToWriteB -= bytesToWrite;
Log?.Progress(write, writes, "Writing padding data...", false);
write++;
}
}
finally
{
Log?.Progress(write, writes, "Writing padding data...", true);
}
}
private void OpenReaders()
{
if (_binaryReader == null)
{
_fileStream = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read);
_binaryReader = new BinaryReader(_fileStream);
}
}
private void CloseReaders()
{
if (_binaryReader != null && _binaryReader.BaseStream != null)
_binaryReader.Close();
_binaryReader = null;
_fileStream = null;
GC.Collect();
}
private void ReadHeader()
{
try
{
OpenReaders();
try
{
// Attempt without key area
bool success = CheckAndReadHeader(false);
if (!success)
{
// Attempt with key area
success = CheckAndReadHeader(true);
}
_fileOK = success;
}
finally
{
CloseReaders();
}
}
catch (Exception ex)
{
Log?.Write(LogType.Error, ex.Message);
_fileOK = false;
_dataSizeB = 0;
_cartSizeB = 0;
_fileSizeB = 0;
_offsetB = 0;
}
}
private bool CheckAndReadHeader(bool assumeKeyArea)
{
// Read file size
_fileSizeB = _fileStream.Length;
if (_fileSizeB < 32 * 1024)
{
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small");
return false;
}
// Setup offset
_offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0);
// Check header
Pos = _offsetB + XCIFileTrimmer.HeaderFilePos;
string head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4));
if (head != XCIFileTrimmer.HeaderMagicValue)
{
if (!assumeKeyArea)
{
Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area...");
}
else
{
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted");
}
return false;
}
// Read Cart Size
Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos;
byte cartSizeId = _binaryReader.ReadByte();
if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB))
{
Log?.Write(LogType.Error, $"The source file doesn't look like an XCI file as the Cartridge Size is incorrect (0x{cartSizeId:X2})");
return false;
}
_cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte;
// Read data size
Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos;
long records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0);
_dataSizeB = RecordsToByte(records);
return true;
}
}
}

View File

@ -0,0 +1,482 @@
using ARMeilleure.Memory;
using Ryujinx.Common;
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using static Ryujinx.Cpu.MemoryEhMeilleure;
namespace ARMeilleure.Common
{
/// <summary>
/// Represents a table of guest address to a value.
/// </summary>
/// <typeparam name="TEntry">Type of the value</typeparam>
public unsafe class AddressTable<TEntry> : IAddressTable<TEntry> where TEntry : unmanaged
{
/// <summary>
/// Represents a page of the address table.
/// </summary>
private readonly struct AddressTablePage
{
/// <summary>
/// True if the allocation belongs to a sparse block, false otherwise.
/// </summary>
public readonly bool IsSparse;
/// <summary>
/// Base address for the page.
/// </summary>
public readonly IntPtr Address;
public AddressTablePage(bool isSparse, IntPtr address)
{
IsSparse = isSparse;
Address = address;
}
}
/// <summary>
/// A sparsely mapped block of memory with a signal handler to map pages as they're accessed.
/// </summary>
private readonly struct TableSparseBlock : IDisposable
{
public readonly SparseMemoryBlock Block;
private readonly TrackingEventDelegate _trackingEvent;
public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
{
var block = new SparseMemoryBlock(size, pageInit, null);
_trackingEvent = (ulong address, ulong size, bool write) =>
{
ulong pointer = (ulong)block.Block.Pointer + address;
ensureMapped((IntPtr)pointer);
return pointer;
};
bool added = NativeSignalHandler.AddTrackedRegion(
(nuint)block.Block.Pointer,
(nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added)
{
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
}
Block = block;
}
public void Dispose()
{
NativeSignalHandler.RemoveTrackedRegion((nuint)Block.Block.Pointer);
Block.Dispose();
}
}
private bool _disposed;
private TEntry** _table;
private readonly List<AddressTablePage> _pages;
private TEntry _fill;
private readonly MemoryBlock _sparseFill;
private readonly SparseMemoryBlock _fillBottomLevel;
private readonly TEntry* _fillBottomLevelPtr;
private readonly List<TableSparseBlock> _sparseReserved;
private readonly ReaderWriterLockSlim _sparseLock;
private ulong _sparseBlockSize;
private ulong _sparseReservedOffset;
public bool Sparse { get; }
/// <inheritdoc/>
public ulong Mask { get; }
/// <inheritdoc/>
public AddressTableLevel[] Levels { get; }
/// <inheritdoc/>
public TEntry Fill
{
get
{
return _fill;
}
set
{
UpdateFill(value);
}
}
/// <inheritdoc/>
public IntPtr Base
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_pages)
{
return (IntPtr)GetRootPage();
}
}
}
/// <summary>
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
/// <see cref="Level"/>.
/// </summary>
/// <param name="levels">Levels for the address table</param>
/// <param name="sparse">True if the bottom page should be sparsely mapped</param>
/// <exception cref="ArgumentNullException"><paramref name="levels"/> is null</exception>
/// <exception cref="ArgumentException">Length of <paramref name="levels"/> is less than 2</exception>
public AddressTable(AddressTableLevel[] levels, bool sparse)
{
ArgumentNullException.ThrowIfNull(levels);
_pages = new List<AddressTablePage>(capacity: 16);
Levels = levels;
Mask = 0;
foreach (var level in Levels)
{
Mask |= level.Mask;
}
Sparse = sparse;
if (sparse)
{
// If the address table is sparse, allocate a fill block
_sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul
ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry);
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
_sparseReserved = new List<TableSparseBlock>();
_sparseLock = new ReaderWriterLockSlim();
_sparseBlockSize = bottomLevelSize;
}
}
/// <summary>
/// Create an <see cref="AddressTable{TEntry}"/> instance for an ARM function table.
/// Selects the best table structure for A32/A64, taking into account the selected memory manager type.
/// </summary>
/// <param name="for64Bits">True if the guest is A64, false otherwise</param>
/// <param name="type">Memory manager type</param>
/// <returns>An <see cref="AddressTable{TEntry}"/> for ARM function lookup</returns>
public static AddressTable<TEntry> CreateForArm(bool for64Bits, MemoryManagerType type)
{
// Assume software memory means that we don't want to use any signal handlers.
bool sparse = type != MemoryManagerType.SoftwareMmu && type != MemoryManagerType.SoftwarePageTable;
return new AddressTable<TEntry>(AddressTablePresets.GetArmPreset(for64Bits, sparse), sparse);
}
/// <summary>
/// Update the fill value for the bottom level of the table.
/// </summary>
/// <param name="fillValue">New fill value</param>
private void UpdateFill(TEntry fillValue)
{
if (_sparseFill != null)
{
Span<byte> span = _sparseFill.GetSpan(0, (int)_sparseFill.Size);
MemoryMarshal.Cast<byte, TEntry>(span).Fill(fillValue);
}
_fill = fillValue;
}
/// <summary>
/// Signal that the given code range exists.
/// </summary>
/// <param name="address"></param>
/// <param name="size"></param>
public void SignalCodeRange(ulong address, ulong size)
{
AddressTableLevel bottom = Levels.Last();
ulong bottomLevelEntries = 1ul << bottom.Length;
ulong entryIndex = address >> bottom.Index;
ulong entries = size >> bottom.Index;
entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries);
_sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry));
}
/// <inheritdoc/>
public bool IsValid(ulong address)
{
return (address & ~Mask) == 0;
}
/// <inheritdoc/>
public ref TEntry GetValue(ulong address)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!IsValid(address))
{
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
}
lock (_pages)
{
TEntry* page = GetPage(address);
int index = Levels[^1].GetValue(address);
EnsureMapped((IntPtr)(page + index));
return ref page[index];
}
}
/// <summary>
/// Gets the leaf page for the specified guest <paramref name="address"/>.
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Leaf page for the specified guest <paramref name="address"/></returns>
private TEntry* GetPage(ulong address)
{
TEntry** page = GetRootPage();
for (int i = 0; i < Levels.Length - 1; i++)
{
ref AddressTableLevel level = ref Levels[i];
ref TEntry* nextPage = ref page[level.GetValue(address)];
if (nextPage == null || nextPage == _fillBottomLevelPtr)
{
ref AddressTableLevel nextLevel = ref Levels[i + 1];
if (i == Levels.Length - 2)
{
nextPage = (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true);
}
else
{
nextPage = (TEntry*)Allocate(1 << nextLevel.Length, GetFillValue(i), leaf: false);
}
}
page = (TEntry**)nextPage;
}
return (TEntry*)page;
}
/// <summary>
/// Ensure the given pointer is mapped in any overlapping sparse reservations.
/// </summary>
/// <param name="ptr">Pointer to be mapped</param>
private void EnsureMapped(IntPtr ptr)
{
if (Sparse)
{
// Check sparse allocations to see if the pointer is in any of them.
// Ensure the page is committed if there's a match.
_sparseLock.EnterReadLock();
try
{
foreach (TableSparseBlock reserved in _sparseReserved)
{
SparseMemoryBlock sparse = reserved.Block;
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
{
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
break;
}
}
}
finally
{
_sparseLock.ExitReadLock();
}
}
}
/// <summary>
/// Get the fill value for a non-leaf level of the table.
/// </summary>
/// <param name="level">Level to get the fill value for</param>
/// <returns>The fill value</returns>
private IntPtr GetFillValue(int level)
{
if (_fillBottomLevel != null && level == Levels.Length - 2)
{
return (IntPtr)_fillBottomLevelPtr;
}
else
{
return IntPtr.Zero;
}
}
/// <summary>
/// Lazily initialize and get the root page of the <see cref="AddressTable{TEntry}"/>.
/// </summary>
/// <returns>Root page of the <see cref="AddressTable{TEntry}"/></returns>
private TEntry** GetRootPage()
{
if (_table == null)
{
if (Levels.Length == 1)
_table = (TEntry**)Allocate(1 << Levels[0].Length, Fill, leaf: true);
else
_table = (TEntry**)Allocate(1 << Levels[0].Length, GetFillValue(0), leaf: false);
}
return _table;
}
/// <summary>
/// Initialize a leaf page with the fill value.
/// </summary>
/// <param name="page">Page to initialize</param>
private void InitLeafPage(Span<byte> page)
{
MemoryMarshal.Cast<byte, TEntry>(page).Fill(_fill);
}
/// <summary>
/// Reserve a new sparse block, and add it to the list.
/// </summary>
/// <returns>The new sparse block that was added</returns>
private TableSparseBlock ReserveNewSparseBlock()
{
var block = new TableSparseBlock(_sparseBlockSize, EnsureMapped, InitLeafPage);
_sparseReserved.Add(block);
_sparseReservedOffset = 0;
return block;
}
/// <summary>
/// Allocates a block of memory of the specified type and length.
/// </summary>
/// <typeparam name="T">Type of elements</typeparam>
/// <param name="length">Number of elements</param>
/// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns>
private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{
var size = sizeof(T) * length;
AddressTablePage page;
if (Sparse && leaf)
{
_sparseLock.EnterWriteLock();
SparseMemoryBlock block;
if (_sparseReserved.Count == 0)
{
block = ReserveNewSparseBlock().Block;
}
else
{
block = _sparseReserved.Last().Block;
if (_sparseReservedOffset == block.Block.Size)
{
block = ReserveNewSparseBlock().Block;
}
}
page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
_sparseReservedOffset += (ulong)size;
_sparseLock.ExitWriteLock();
}
else
{
var address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
page = new AddressTablePage(false, address);
var span = new Span<T>((void*)page.Address, length);
span.Fill(fill);
}
_pages.Add(page);
//TranslatorEventSource.Log.AddressTableAllocated(size, leaf);
return page.Address;
}
/// <summary>
/// Releases all resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases all unmanaged and optionally managed resources used by the <see cref="AddressTable{TEntry}"/>
/// instance.
/// </summary>
/// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
foreach (var page in _pages)
{
if (!page.IsSparse)
{
Marshal.FreeHGlobal(page.Address);
}
}
if (Sparse)
{
foreach (TableSparseBlock block in _sparseReserved)
{
block.Dispose();
}
_sparseReserved.Clear();
_fillBottomLevel.Dispose();
_sparseFill.Dispose();
_sparseLock.Dispose();
}
_disposed = true;
}
}
/// <summary>
/// Frees resources used by the <see cref="AddressTable{TEntry}"/> instance.
/// </summary>
~AddressTable()
{
Dispose(false);
}
}
}

View File

@ -32,7 +32,7 @@ namespace Ryujinx.Cpu.AppleHv
{
}
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new DummyDiskCacheLoadState();
}

View File

@ -48,7 +48,7 @@ namespace Ryujinx.Cpu
/// <param name="displayVersion">Version of the application</param>
/// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param>
/// <returns>Disk cache load progress reporter and manager</returns>
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled);
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector);
/// <summary>
/// Indicates that code has been loaded into guest memory, and that it might be executed in the future.

View File

@ -1,3 +1,4 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.Translation;
using Ryujinx.Cpu.Signal;
@ -9,11 +10,13 @@ namespace Ryujinx.Cpu.Jit
{
private readonly ITickSource _tickSource;
private readonly Translator _translator;
private readonly AddressTable<ulong> _functionTable;
public JitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit)
{
_tickSource = tickSource;
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit);
_functionTable = AddressTable<ulong>.CreateForArm(for64Bit, memory.Type);
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, _functionTable);
if (memory.Type.IsHostMappedOrTracked())
{
@ -47,14 +50,15 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled));
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector));
}
/// <inheritdoc/>
public void PrepareCodeRange(ulong address, ulong size)
{
_functionTable.SignalCodeRange(address, size);
_translator.PrepareCodeRange(address, size);
}

View File

@ -140,6 +140,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
bool isTail = false)
{
int tempRegister;
int tempGuestAddress = -1;
bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
funcTable is { Sparse: true };
if (guestAddress.Kind == OperandKind.Constant)
{
@ -153,9 +157,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
else
{
asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset);
if (inlineLookup && guestAddress.Value == 0)
{
// X0 will be overwritten. Move the address to a temp register.
tempGuestAddress = regAlloc.AllocateTempGprRegister();
asm.Mov(Register(tempGuestAddress), guestAddress);
}
}
tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1;
tempRegister = NextFreeRegister(1, tempGuestAddress);
if (!isTail)
{
@ -176,6 +187,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
asm.Mov(rn, funcPtrLoc & ~0xfffUL);
asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL));
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
if (tempGuestAddress != -1)
{
guestAddress = Register(tempGuestAddress);
}
ulong tableBase = (ulong)funcTable.Base;
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
var level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
if (tempGuestAddress != -1)
{
regAlloc.FreeTempGprRegister(tempGuestAddress);
}
}
else
{
asm.Mov(rn, (ulong)funcPtr);
@ -252,5 +297,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
{
return new Operand(register, RegisterType.Integer, type);
}
private static Operand Const(long value, OperandType type = OperandType.I64)
{
return new Operand(type, (ulong)value);
}
private static int NextFreeRegister(int start, int avoid)
{
if (start == avoid)
{
start++;
}
return start;
}
}
}

View File

@ -305,6 +305,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
bool isTail = false)
{
int tempRegister;
int tempGuestAddress = -1;
bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
funcTable is { Sparse: true };
if (guestAddress.Kind == OperandKind.Constant)
{
@ -318,9 +322,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
else
{
asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset);
if (inlineLookup && guestAddress.Value == 0)
{
// X0 will be overwritten. Move the address to a temp register.
tempGuestAddress = regAlloc.AllocateTempGprRegister();
asm.Mov(Register(tempGuestAddress), guestAddress);
}
}
tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1;
tempRegister = NextFreeRegister(1, tempGuestAddress);
if (!isTail)
{
@ -341,6 +352,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
asm.Mov(rn, funcPtrLoc & ~0xfffUL);
asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL));
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
if (tempGuestAddress != -1)
{
guestAddress = Register(tempGuestAddress);
}
ulong tableBase = (ulong)funcTable.Base;
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
var level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
if (tempGuestAddress != -1)
{
regAlloc.FreeTempGprRegister(tempGuestAddress);
}
}
else
{
asm.Mov(rn, (ulong)funcPtr);
@ -613,5 +658,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
{
return new Operand(register, RegisterType.Integer, type);
}
private static Operand Const(long value, OperandType type = OperandType.I64)
{
return new Operand(type, (ulong)value);
}
private static int NextFreeRegister(int start, int avoid)
{
if (start == avoid)
{
start++;
}
return start;
}
}
}

View File

@ -1,3 +1,4 @@
using ARMeilleure.Common;
using ARMeilleure.Memory;
using Ryujinx.Cpu.Jit;
using Ryujinx.Cpu.LightningJit.State;
@ -8,11 +9,16 @@ namespace Ryujinx.Cpu.LightningJit
{
private readonly ITickSource _tickSource;
private readonly Translator _translator;
private readonly AddressTable<ulong> _functionTable;
public LightningJitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit)
{
_tickSource = tickSource;
_translator = new Translator(memory, for64Bit);
_functionTable = AddressTable<ulong>.CreateForArm(for64Bit, memory.Type);
_translator = new Translator(memory, _functionTable);
memory.UnmapEvent += UnmapHandler;
}
@ -40,7 +46,7 @@ namespace Ryujinx.Cpu.LightningJit
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
{
return new DummyDiskCacheLoadState();
}
@ -48,6 +54,7 @@ namespace Ryujinx.Cpu.LightningJit
/// <inheritdoc/>
public void PrepareCodeRange(ulong address, ulong size)
{
_functionTable.SignalCodeRange(address, size);
}
public void Dispose()

View File

@ -19,25 +19,6 @@ namespace Ryujinx.Cpu.LightningJit
// Should be enabled on platforms that enforce W^X.
private static bool IsNoWxPlatform => false;
private static readonly AddressTable<ulong>.Level[] _levels64Bit =
new AddressTable<ulong>.Level[]
{
new(31, 17),
new(23, 8),
new(15, 8),
new( 7, 8),
new( 2, 5),
};
private static readonly AddressTable<ulong>.Level[] _levels32Bit =
new AddressTable<ulong>.Level[]
{
new(23, 9),
new(15, 8),
new( 7, 8),
new( 1, 6),
};
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly NoWxCache _noWxCache;
private bool _disposed;
@ -47,7 +28,7 @@ namespace Ryujinx.Cpu.LightningJit
internal TranslatorStubs Stubs { get; }
internal IMemoryManager Memory { get; }
public Translator(IMemoryManager memory, bool for64Bits)
public Translator(IMemoryManager memory, AddressTable<ulong> functionTable)
{
Memory = memory;
@ -63,7 +44,7 @@ namespace Ryujinx.Cpu.LightningJit
}
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;

View File

@ -23,7 +23,7 @@ namespace Ryujinx.Cpu.LightningJit
private bool _disposed;
private readonly AddressTable<ulong> _functionTable;
private readonly IAddressTable<ulong> _functionTable;
private readonly NoWxCache _noWxCache;
private readonly GetFunctionAddressDelegate _getFunctionAddressRef;
private readonly nint _getFunctionAddress;
@ -79,7 +79,7 @@ namespace Ryujinx.Cpu.LightningJit
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <param name="noWxCache">Cache used on platforms that enforce W^X, otherwise should be null</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(AddressTable<ulong> functionTable, NoWxCache noWxCache)
public TranslatorStubs(IAddressTable<ulong> functionTable, NoWxCache noWxCache)
{
ArgumentNullException.ThrowIfNull(functionTable);

View File

@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
IPipeline Pipeline { get; }
IWindow Window { get; }
uint ProgramCount { get; }
void BackgroundContextAction(Action action, bool alwaysBackground = false);

View File

@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
ProgramCount++;
return new Program(shaders, info.FragmentOutputMap);
}

View File

@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan
if (_handle != BufferHandle.Null)
{
// May need to restride the vertex buffer.
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && (_stride % alignment) != 0)
//
// Fix divide by zero when recovering from missed draw (Oct. 16 2024)
// (fixes crash in 'Baldo: The Guardian Owls' opening cutscene)
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && alignment != 0 && (_stride % alignment) != 0)
{
autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment);

View File

@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan
public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info)
{
ProgramCount++;
bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute;
if (info.State.HasValue || isCompute)

View File

@ -13,6 +13,7 @@ namespace Ryujinx.HLE.Generators
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
CodeGenerator generator = new CodeGenerator();
generator.AppendLine("#nullable enable");
generator.AppendLine("using System;");
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
generator.EnterScope($"partial class IUserInterface");
@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators
generator.LeaveScope();
generator.LeaveScope();
generator.AppendLine("#nullable disable");
context.AddSource($"IUserInterface.g.cs", generator.ToString());
}

View File

@ -164,6 +164,21 @@ namespace Ryujinx.HLE
/// </summary>
public MultiplayerMode MultiplayerMode { internal get; set; }
/// <summary>
/// Disable P2P mode
/// </summary>
public bool MultiplayerDisableP2p { internal get; set; }
/// <summary>
/// Multiplayer Passphrase
/// </summary>
public string MultiplayerLdnPassphrase { internal get; set; }
/// <summary>
/// LDN Server
/// </summary>
public string MultiplayerLdnServer { internal get; set; }
/// <summary>
/// An action called when HLE force a refresh of output after docked mode changed.
/// </summary>
@ -194,7 +209,10 @@ namespace Ryujinx.HLE
float audioVolume,
bool useHypervisor,
string multiplayerLanInterfaceId,
MultiplayerMode multiplayerMode)
MultiplayerMode multiplayerMode,
bool multiplayerDisableP2p,
string multiplayerLdnPassphrase,
string multiplayerLdnServer)
{
VirtualFileSystem = virtualFileSystem;
LibHacHorizonManager = libHacHorizonManager;
@ -222,6 +240,9 @@ namespace Ryujinx.HLE
UseHypervisor = useHypervisor;
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
MultiplayerMode = multiplayerMode;
MultiplayerDisableP2p = multiplayerDisableP2p;
MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
MultiplayerLdnServer = multiplayerLdnServer;
}
}
}

View File

@ -1,4 +1,6 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.Browser;
using Ryujinx.HLE.HOS.Applets.Dummy;
using Ryujinx.HLE.HOS.Applets.Error;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
@ -26,9 +28,13 @@ namespace Ryujinx.HLE.HOS.Applets
return new BrowserApplet(system);
case AppletId.LibAppletOff:
return new BrowserApplet(system);
case AppletId.MiiEdit:
Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet");
return new DummyApplet(system);
}
throw new NotImplementedException($"{applet} applet is not implemented.");
Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!");
return new DummyApplet(system);
}
}
}

View File

@ -0,0 +1,43 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.Dummy
{
internal class DummyApplet : IApplet
{
private readonly Horizon _system;
private AppletSession _normalSession;
public event EventHandler AppletStateChanged;
public DummyApplet(Horizon system)
{
_system = system;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
_normalSession.Push(BuildResponse());
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
return ResultCode.Success;
}
private static T ReadStruct<T>(byte[] data) where T : struct
{
return MemoryMarshal.Read<T>(data.AsSpan());
}
private static byte[] BuildResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)ResultCode.Success);
return stream.ToArray();
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
}
}

View File

@ -13,7 +13,8 @@ namespace Ryujinx.HLE.HOS
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize);
ulong codeSize,
string cacheSelector);
}
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
@ -67,10 +68,11 @@ namespace Ryujinx.HLE.HOS
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
ulong codeSize,
string cacheSelector)
{
_cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector);
}
public void InvalidateCacheRegion(ulong address, ulong size)

View File

@ -114,7 +114,7 @@ namespace Ryujinx.HLE.HOS
}
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize);
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles
return processContext;
}

View File

@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler
return ParseIntegerLiteral("unsigned short");
case 'i':
_position++;
return ParseIntegerLiteral("");
return ParseIntegerLiteral(string.Empty);
case 'j':
_position++;
return ParseIntegerLiteral("u");

View File

@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS
private readonly Dictionary<ulong, ModCache> _appMods; // key is ApplicationId
private PatchCache _patches;
private static readonly EnumerationOptions _dirEnumOptions;
static ModLoader()
private static readonly EnumerationOptions _dirEnumOptions = new()
{
_dirEnumOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
MatchType = MatchType.Simple,
RecurseSubdirectories = false,
ReturnSpecialDirectories = false,
};
}
MatchCasing = MatchCasing.CaseInsensitive,
MatchType = MatchType.Simple,
RecurseSubdirectories = false,
ReturnSpecialDirectories = false,
};
public ModLoader()
{
@ -169,7 +164,7 @@ namespace Ryujinx.HLE.HOS
foreach (var modDir in dir.EnumerateDirectories())
{
types.Clear();
Mod<DirectoryInfo> mod = new("", null, true);
Mod<DirectoryInfo> mod = new(string.Empty, null, true);
if (StrEquals(RomfsDir, modDir.Name))
{

View File

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
{
@ -25,5 +26,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
return ResultCode.Success;
}
[CommandCmif(350)]
// OpenSystemApplicationProxy(u64, pid, handle<copy>) -> object<nn::am::service::IApplicationProxy>
public ResultCode OpenSystemApplicationProxy(ServiceCtx context)
{
MakeObject(context, new IApplicationProxy(context.Request.HandleDesc.PId));
return ResultCode.Success;
}
}
}

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)]
struct NetworkConfig
{
public IntentId IntentId;

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x60)]
[StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)]
struct ScanFilter
{
public NetworkId NetworkId;

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x44)]
[StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)]
struct SecurityConfig
{
public SecurityMode SecurityMode;

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
struct SecurityParameter
{
public Array16<byte> Data;

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x30)]
[StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)]
struct UserConfig
{
public Array33<byte> UserName;

View File

@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public Array8<NodeLatestUpdate> LatestUpdates = new();
public bool Connected { get; private set; }
public ProxyConfig Config => _parent.NetworkClient.Config;
public AccessPoint(IUserLocalCommunicationService parent)
{
_parent = parent;
@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public void Dispose()
{
_parent.NetworkClient.DisconnectNetwork();
if (_parent?.NetworkClient != null)
{
_parent.NetworkClient.DisconnectNetwork();
_parent.NetworkClient.NetworkChange -= NetworkChanged;
_parent.NetworkClient.NetworkChange -= NetworkChanged;
}
}
private void NetworkChanged(object sender, NetworkChangeEventArgs e)

View File

@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
ProxyConfig Config { get; }
bool NeedsRealId { get; }
event EventHandler<NetworkChangeEventArgs> NetworkChange;

View File

@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class IUserLocalCommunicationService : IpcService, IDisposable
{
public static string DefaultLanPlayHost = "ryuldn.vudjun.com";
public static short LanPlayPort = 30456;
public INetworkClient NetworkClient { get; private set; }
private const int NifmRequestID = 90;
@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
{
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
if (unicastAddress == null)
ProxyConfig config = _state switch
{
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
NetworkState.AccessPointCreated => _accessPoint.Config,
NetworkState.StationConnected => _station.Config,
_ => default
};
if (config.ProxyIp == 0)
{
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
if (unicastAddress == null)
{
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
}
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
context.ResponseData.Write(config.ProxyIp);
context.ResponseData.Write(config.ProxySubnetMask);
}
}
else
@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
switch (mode)
{
case MultiplayerMode.LdnRyu:
try
{
string ldnServer = context.Device.Configuration.MultiplayerLdnServer;
if (string.IsNullOrEmpty(ldnServer))
{
ldnServer = DefaultLanPlayHost;
}
if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress))
{
ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0];
}
NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration);
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless.");
Logger.Error?.Print(LogClass.ServiceLdn, ex.Message);
NetworkClient = new LdnDisabledClient();
}
break;
case MultiplayerMode.LdnMitm:
NetworkClient = new LdnMitmClient(context.Device.Configuration);
break;
@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_accessPoint?.Dispose();
_accessPoint = null;
NetworkClient?.Dispose();
NetworkClient?.DisconnectAndStop();
NetworkClient = null;
}
}

View File

@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class LdnDisabledClient : INetworkClient
{
public ProxyConfig Config { get; }
public bool NeedsRealId => true;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
public NetworkError Connect(ConnectRequest request)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return NetworkError.None;
@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return NetworkError.None;
@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return true;
@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return true;
@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!");
return Array.Empty<NetworkInfo>();
}

View File

@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
/// </summary>
internal class LdnMitmClient : INetworkClient
{
public ProxyConfig Config { get; }
public bool NeedsRealId => false;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;

View File

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
interface IProxyClient
{
bool SendAsync(byte[] buffer);
}
}

View File

@ -0,0 +1,645 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TcpClient = NetCoreServer.TcpClient;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient
{
public bool NeedsRealId => true;
private static InitializeMessage InitializeMemory = new InitializeMessage();
private const int InactiveTimeout = 6000;
private const int FailureTimeout = 4000;
private const int ScanTimeout = 1000;
private bool _useP2pProxy;
private NetworkError _lastError;
private readonly ManualResetEvent _connected = new ManualResetEvent(false);
private readonly ManualResetEvent _error = new ManualResetEvent(false);
private readonly ManualResetEvent _scan = new ManualResetEvent(false);
private readonly ManualResetEvent _reject = new ManualResetEvent(false);
private readonly AutoResetEvent _apConnected = new AutoResetEvent(false);
private readonly RyuLdnProtocol _protocol;
private readonly NetworkTimeout _timeout;
private readonly List<NetworkInfo> _availableGames = new List<NetworkInfo>();
private DisconnectReason _disconnectReason;
private P2pProxyServer _hostedProxy;
private P2pProxyClient _connectedProxy;
private bool _networkConnected;
private string _passphrase;
private byte[] _gameVersion = new byte[0x10];
private readonly HLEConfiguration _config;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
public ProxyConfig Config { get; private set; }
public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port)
{
if (ProxyHelpers.SupportsNoDelay())
{
OptionNoDelay = true;
}
_protocol = new RyuLdnProtocol();
_timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection);
_protocol.Initialize += HandleInitialize;
_protocol.Connected += HandleConnected;
_protocol.Reject += HandleReject;
_protocol.RejectReply += HandleRejectReply;
_protocol.SyncNetwork += HandleSyncNetwork;
_protocol.ProxyConfig += HandleProxyConfig;
_protocol.Disconnected += HandleDisconnected;
_protocol.ScanReply += HandleScanReply;
_protocol.ScanReplyEnd += HandleScanReplyEnd;
_protocol.ExternalProxy += HandleExternalProxy;
_protocol.Ping += HandlePing;
_protocol.NetworkError += HandleNetworkError;
_config = config;
_useP2pProxy = !config.MultiplayerDisableP2p;
}
private void TimeoutConnection()
{
_connected.Reset();
DisconnectAsync();
while (IsConnected)
{
Thread.Yield();
}
}
private bool EnsureConnected()
{
if (IsConnected)
{
return true;
}
_error.Reset();
ConnectAsync();
int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout);
if (IsConnected)
{
SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory));
}
return index == 0 && IsConnected;
}
private void UpdatePassphraseIfNeeded()
{
string passphrase = _config.MultiplayerLdnPassphrase ?? "";
if (passphrase != _passphrase)
{
_passphrase = passphrase;
SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8)));
}
}
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}");
UpdatePassphraseIfNeeded();
_connected.Set();
}
protected override void OnDisconnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}");
_passphrase = null;
_connected.Reset();
if (_networkConnected)
{
DisconnectInternal();
}
}
public void DisconnectAndStop()
{
_timeout.Dispose();
DisconnectAsync();
while (IsConnected)
{
Thread.Yield();
}
Dispose();
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
_protocol.Read(buffer, (int)offset, (int)size);
}
protected override void OnError(SocketError error)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}");
_error.Set();
}
private void HandleInitialize(LdnHeader header, InitializeMessage initialize)
{
InitializeMemory = initialize;
}
private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config)
{
int length = config.AddressFamily switch
{
AddressFamily.InterNetwork => 4,
AddressFamily.InterNetworkV6 => 16,
_ => 0
};
if (length == 0)
{
return; // Invalid external proxy.
}
IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray());
P2pProxyClient proxy = new(address.ToString(), config.ProxyPort);
_connectedProxy = proxy;
bool success = proxy.PerformAuth(config);
if (!success)
{
DisconnectInternal();
}
}
private void HandlePing(LdnHeader header, PingMessage ping)
{
if (ping.Requester == 0) // Server requested.
{
// Send the ping message back.
SendAsync(_protocol.Encode(PacketId.Ping, ping));
}
}
private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error)
{
if (error.Error == NetworkError.PortUnreachable)
{
_useP2pProxy = false;
}
else
{
_lastError = error.Error;
}
}
private NetworkError ConsumeNetworkError()
{
NetworkError result = _lastError;
_lastError = NetworkError.None;
return result;
}
private void HandleSyncNetwork(LdnHeader header, NetworkInfo info)
{
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
}
private void HandleConnected(LdnHeader header, NetworkInfo info)
{
_networkConnected = true;
_disconnectReason = DisconnectReason.None;
_apConnected.Set();
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
}
private void HandleDisconnected(LdnHeader header, DisconnectMessage message)
{
DisconnectInternal();
}
private void HandleReject(LdnHeader header, RejectRequest reject)
{
// When the client receives a Reject request, we have been rejected and will be disconnected shortly.
_disconnectReason = reject.DisconnectReason;
}
private void HandleRejectReply(LdnHeader header)
{
_reject.Set();
}
private void HandleScanReply(LdnHeader header, NetworkInfo info)
{
_availableGames.Add(info);
}
private void HandleScanReplyEnd(LdnHeader obj)
{
_scan.Set();
}
private void DisconnectInternal()
{
if (_networkConnected)
{
_networkConnected = false;
_hostedProxy?.Dispose();
_hostedProxy = null;
_connectedProxy?.Dispose();
_connectedProxy = null;
_apConnected.Reset();
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason));
if (IsConnected)
{
_timeout.RefreshTimeout();
}
}
}
public void DisconnectNetwork()
{
if (_networkConnected)
{
SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage()));
DisconnectInternal();
}
}
public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
{
if (_networkConnected)
{
_reject.Reset();
SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId)));
int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout);
if (index == 0)
{
return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success;
}
}
return ResultCode.InvalidState;
}
public void SetAdvertiseData(byte[] data)
{
// TODO: validate we're the owner (the server will do this anyways tho)
if (_networkConnected)
{
SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data));
}
}
public void SetGameVersion(byte[] versionString)
{
_gameVersion = versionString;
if (_gameVersion.Length < 0x10)
{
Array.Resize(ref _gameVersion, 0x10);
}
}
public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
{
// TODO: validate we're the owner (the server will do this anyways tho)
if (_networkConnected)
{
SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest
{
StationAcceptPolicy = acceptPolicy
}));
}
}
private void DisposeProxy()
{
_hostedProxy?.Dispose();
_hostedProxy = null;
}
private void ConfigureAccessPoint(ref RyuNetworkConfig request)
{
_gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan());
if (_useP2pProxy)
{
// Before sending the request, attempt to set up a proxy server.
// This can be on a range of private ports, which can be exposed on a range of public
// ports via UPnP. If any of this fails, we just fall back to using the master server.
int i = 0;
for (; i < P2pProxyServer.PrivatePortRange; i++)
{
_hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol);
try
{
_hostedProxy.Start();
break;
}
catch (SocketException e)
{
_hostedProxy.Dispose();
_hostedProxy = null;
if (e.SocketErrorCode != SocketError.AddressAlreadyInUse)
{
i = P2pProxyServer.PrivatePortRange; // Immediately fail.
}
}
}
bool openSuccess = i < P2pProxyServer.PrivatePortRange;
if (openSuccess)
{
Task<ushort> natPunchResult = _hostedProxy.NatPunch();
try
{
if (natPunchResult.Result != 0)
{
// Tell the server that we are hosting the proxy.
request.ExternalProxyPort = natPunchResult.Result;
}
}
catch (Exception) { }
if (request.ExternalProxyPort == 0)
{
Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency.");
_hostedProxy.Dispose();
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}.");
_hostedProxy.Start();
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface();
unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan());
request.InternalProxyPort = _hostedProxy.PrivatePort;
request.AddressFamily = unicastAddress.Address.AddressFamily;
}
}
else
{
Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency.");
}
}
}
private bool CreateNetworkCommon()
{
bool signalled = _apConnected.WaitOne(FailureTimeout);
if (!_useP2pProxy && _hostedProxy != null)
{
Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency.");
DisposeProxy();
}
if (signalled && _connectedProxy != null)
{
_connectedProxy.EnsureProxyReady();
Config = _connectedProxy.ProxyConfig;
}
else
{
DisposeProxy();
}
return signalled;
}
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
{
_timeout.DisableTimeout();
ConfigureAccessPoint(ref request.RyuNetworkConfig);
if (!EnsureConnected())
{
DisposeProxy();
return false;
}
UpdatePassphraseIfNeeded();
SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData));
// Send a network change event with dummy data immediately. Necessary to avoid crashes in some games
var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo()
{
Common = new CommonNetworkInfo()
{
MacAddress = InitializeMemory.MacAddress,
Channel = request.NetworkConfig.Channel,
LinkLevel = 3,
NetworkType = 2,
Ssid = new Ssid()
{
Length = 32
}
},
Ldn = new LdnNetworkInfo()
{
AdvertiseDataSize = (ushort)advertiseData.Length,
AuthenticationId = 0,
NodeCount = 1,
NodeCountMax = request.NetworkConfig.NodeCountMax,
SecurityMode = (ushort)request.SecurityConfig.SecurityMode
}
}, true);
networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo()
{
Ipv4Address = 175243265,
IsConnected = 1,
LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion,
MacAddress = InitializeMemory.MacAddress,
NodeId = 0,
UserName = request.UserConfig.UserName
};
"12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan());
NetworkChange?.Invoke(this, networkChangeEvent);
return CreateNetworkCommon();
}
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
{
_timeout.DisableTimeout();
ConfigureAccessPoint(ref request.RyuNetworkConfig);
if (!EnsureConnected())
{
DisposeProxy();
return false;
}
UpdatePassphraseIfNeeded();
SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData));
return CreateNetworkCommon();
}
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
{
if (!_networkConnected)
{
_timeout.RefreshTimeout();
}
_availableGames.Clear();
int index = -1;
if (EnsureConnected())
{
UpdatePassphraseIfNeeded();
_scan.Reset();
SendAsync(_protocol.Encode(PacketId.Scan, scanFilter));
index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout);
}
if (index != 0)
{
// An error occurred or timeout. Write 0 games.
return Array.Empty<NetworkInfo>();
}
return _availableGames.ToArray();
}
private NetworkError ConnectCommon()
{
bool signalled = _apConnected.WaitOne(FailureTimeout);
NetworkError error = ConsumeNetworkError();
if (error != NetworkError.None)
{
return error;
}
if (signalled && _connectedProxy != null)
{
_connectedProxy.EnsureProxyReady();
Config = _connectedProxy.ProxyConfig;
}
return signalled ? NetworkError.None : NetworkError.ConnectTimeout;
}
public NetworkError Connect(ConnectRequest request)
{
_timeout.DisableTimeout();
if (!EnsureConnected())
{
return NetworkError.Unknown;
}
SendAsync(_protocol.Encode(PacketId.Connect, request));
var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo()
{
Common = request.NetworkInfo.Common,
Ldn = request.NetworkInfo.Ldn
}, true);
NetworkChange?.Invoke(this, networkChangeEvent);
return ConnectCommon();
}
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
{
_timeout.DisableTimeout();
if (!EnsureConnected())
{
return NetworkError.Unknown;
}
SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request));
return ConnectCommon();
}
private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
{
Config = config;
SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
class NetworkTimeout : IDisposable
{
private readonly int _idleTimeout;
private readonly Action _timeoutCallback;
private CancellationTokenSource _cancel;
private readonly object _lock = new object();
public NetworkTimeout(int idleTimeout, Action timeoutCallback)
{
_idleTimeout = idleTimeout;
_timeoutCallback = timeoutCallback;
}
private async Task TimeoutTask()
{
CancellationTokenSource cts;
lock (_lock)
{
cts = _cancel;
}
if (cts == null)
{
return;
}
try
{
await Task.Delay(_idleTimeout, cts.Token);
}
catch (TaskCanceledException)
{
return; // Timeout cancelled.
}
lock (_lock)
{
// Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled.
if (cts == _cancel)
{
_timeoutCallback();
}
}
}
public bool RefreshTimeout()
{
lock (_lock)
{
_cancel?.Cancel();
_cancel = new CancellationTokenSource();
Task.Run(TimeoutTask);
}
return true;
}
public void DisableTimeout()
{
lock (_lock)
{
_cancel?.Cancel();
_cancel = new CancellationTokenSource();
}
}
public void Dispose()
{
DisableTimeout();
}
}
}

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
public class EphemeralPortPool
{
private const ushort EphemeralBase = 49152;
private readonly List<ushort> _ephemeralPorts = new List<ushort>();
private readonly object _lock = new object();
public ushort Get()
{
ushort port = EphemeralBase;
lock (_lock)
{
// Starting at the ephemeral port base, return an ephemeral port that is not in use.
// Returns 0 if the range is exhausted.
for (int i = 0; i < _ephemeralPorts.Count; i++)
{
ushort existingPort = _ephemeralPorts[i];
if (existingPort > port)
{
// The port was free - take it.
_ephemeralPorts.Insert(i, port);
return port;
}
port++;
}
if (port != 0)
{
_ephemeralPorts.Add(port);
}
return port;
}
}
public void Return(ushort port)
{
lock (_lock)
{
_ephemeralPorts.Remove(port);
}
}
}
}

View File

@ -0,0 +1,254 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class LdnProxy : IDisposable
{
public EndPoint LocalEndpoint { get; }
public IPAddress LocalAddress { get; }
private readonly List<LdnProxySocket> _sockets = new List<LdnProxySocket>();
private readonly Dictionary<ProtocolType, EphemeralPortPool> _ephemeralPorts = new Dictionary<ProtocolType, EphemeralPortPool>();
private readonly IProxyClient _parent;
private RyuLdnProtocol _protocol;
private readonly uint _subnetMask;
private readonly uint _localIp;
private readonly uint _broadcast;
public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol)
{
_parent = client;
_protocol = protocol;
_ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool();
_ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool();
byte[] address = BitConverter.GetBytes(config.ProxyIp);
Array.Reverse(address);
LocalAddress = new IPAddress(address);
_subnetMask = config.ProxySubnetMask;
_localIp = config.ProxyIp;
_broadcast = _localIp | (~_subnetMask);
RegisterHandlers(protocol);
}
public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol)
{
if (protocol == ProtocolType.Tcp)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested.");
}
return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp);
}
private void RegisterHandlers(RyuLdnProtocol protocol)
{
protocol.ProxyConnect += HandleConnectionRequest;
protocol.ProxyConnectReply += HandleConnectionResponse;
protocol.ProxyData += HandleData;
protocol.ProxyDisconnect += HandleDisconnect;
_protocol = protocol;
}
public void UnregisterHandlers(RyuLdnProtocol protocol)
{
protocol.ProxyConnect -= HandleConnectionRequest;
protocol.ProxyConnectReply -= HandleConnectionResponse;
protocol.ProxyData -= HandleData;
protocol.ProxyDisconnect -= HandleDisconnect;
}
public ushort GetEphemeralPort(ProtocolType type)
{
return _ephemeralPorts[type].Get();
}
public void ReturnEphemeralPort(ProtocolType type, ushort port)
{
_ephemeralPorts[type].Return(port);
}
public void RegisterSocket(LdnProxySocket socket)
{
lock (_sockets)
{
_sockets.Add(socket);
}
}
public void UnregisterSocket(LdnProxySocket socket)
{
lock (_sockets)
{
_sockets.Remove(socket);
}
}
private void ForRoutedSockets(ProxyInfo info, Action<LdnProxySocket> action)
{
lock (_sockets)
{
foreach (LdnProxySocket socket in _sockets)
{
// Must match protocol and destination port.
if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort)
{
continue;
}
// We can assume packets routed to us have been sent to our destination.
// They will either be sent to us, or broadcast packets.
action(socket);
}
}
}
public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request)
{
ForRoutedSockets(request.Info, (socket) =>
{
socket.HandleConnectRequest(request);
});
}
public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response)
{
ForRoutedSockets(response.Info, (socket) =>
{
socket.HandleConnectResponse(response);
});
}
public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data)
{
ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data };
ForRoutedSockets(proxyHeader.Info, (socket) =>
{
socket.IncomingData(packet);
});
}
public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect)
{
ForRoutedSockets(disconnect.Info, (socket) =>
{
socket.HandleDisconnect(disconnect);
});
}
private uint GetIpV4(IPEndPoint endpoint)
{
if (endpoint.AddressFamily != AddressFamily.InterNetwork)
{
throw new NotSupportedException();
}
byte[] address = endpoint.Address.GetAddressBytes();
Array.Reverse(address);
return BitConverter.ToUInt32(address);
}
private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type)
{
return new ProxyInfo
{
SourceIpV4 = GetIpV4(localEp),
SourcePort = (ushort)localEp.Port,
DestIpV4 = GetIpV4(remoteEP),
DestPort = (ushort)remoteEP.Port,
Protocol = type
};
}
public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must ask the other side to initialize a connection, so they can accept a socket for us.
ProxyConnectRequest request = new ProxyConnectRequest
{
Info = MakeInfo(localEp, remoteEp, type)
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request));
}
public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must tell the other side that we have accepted their request for connection.
ProxyConnectResponse request = new ProxyConnectResponse
{
Info = MakeInfo(localEp, remoteEp, type)
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request));
}
public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must tell the other side that our connection is dropped.
ProxyDisconnectMessage request = new ProxyDisconnectMessage
{
Info = MakeInfo(localEp, remoteEp, type),
DisconnectReason = 0 // TODO
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request));
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We send exactly as much as the user wants us to, currently instantly.
// TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp?
ProxyDataHeader request = new ProxyDataHeader
{
Info = MakeInfo(localEp, remoteEp, type),
DataLength = (uint)buffer.Length
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray()));
return buffer.Length;
}
public bool IsBroadcast(uint ip)
{
return ip == _broadcast;
}
public bool IsMyself(uint ip)
{
return ip == _localIp;
}
public void Dispose()
{
UnregisterHandlers(_protocol);
lock (_sockets)
{
foreach (LdnProxySocket socket in _sockets)
{
socket.ProxyDestroyed();
}
}
}
}
}

View File

@ -0,0 +1,797 @@
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
/// <summary>
/// This socket is forwarded through a TCP stream that goes through the Ldn server.
/// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network.
/// </summary>
class LdnProxySocket : ISocketImpl
{
private readonly LdnProxy _proxy;
private bool _isListening;
private readonly List<LdnProxySocket> _listenSockets = new List<LdnProxySocket>();
private readonly Queue<ProxyConnectRequest> _connectRequests = new Queue<ProxyConnectRequest>();
private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false);
private readonly int _acceptTimeout = -1;
private readonly Queue<int> _errors = new Queue<int>();
private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false);
private ProxyConnectResponse _connectResponse;
private int _receiveTimeout = -1;
private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
private readonly Queue<ProxyDataPacket> _receiveQueue = new Queue<ProxyDataPacket>();
// private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used.
private bool _connecting;
private bool _broadcast;
private bool _readShutdown;
// private bool _writeShutdown;
private bool _closed;
private readonly Dictionary<SocketOptionName, int> _socketOptions = new Dictionary<SocketOptionName, int>()
{
{ SocketOptionName.Broadcast, 0 }, //TODO: honor this value
{ SocketOptionName.DontLinger, 0 },
{ SocketOptionName.Debug, 0 },
{ SocketOptionName.Error, 0 },
{ SocketOptionName.KeepAlive, 0 },
{ SocketOptionName.OutOfBandInline, 0 },
{ SocketOptionName.ReceiveBuffer, 131072 },
{ SocketOptionName.ReceiveTimeout, -1 },
{ SocketOptionName.SendBuffer, 131072 },
{ SocketOptionName.SendTimeout, -1 },
{ SocketOptionName.Type, 0 },
{ SocketOptionName.ReuseAddress, 0 } //TODO: honor this value
};
public EndPoint RemoteEndPoint { get; private set; }
public EndPoint LocalEndPoint { get; private set; }
public bool Connected { get; private set; }
public bool IsBound { get; private set; }
public AddressFamily AddressFamily { get; }
public SocketType SocketType { get; }
public ProtocolType ProtocolType { get; }
public bool Blocking { get; set; }
public int Available
{
get
{
int result = 0;
lock (_receiveQueue)
{
foreach (ProxyDataPacket data in _receiveQueue)
{
result += data.Data.Length;
}
}
return result;
}
}
public bool Readable
{
get
{
if (_isListening)
{
lock (_connectRequests)
{
return _connectRequests.Count > 0;
}
}
else
{
if (_readShutdown)
{
return true;
}
lock (_receiveQueue)
{
return _receiveQueue.Count > 0;
}
}
}
}
public bool Writable => Connected || ProtocolType == ProtocolType.Udp;
public bool Error => false;
public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy)
{
AddressFamily = addressFamily;
SocketType = socketType;
ProtocolType = protocolType;
_proxy = proxy;
_socketOptions[SocketOptionName.Type] = (int)socketType;
proxy.RegisterSocket(this);
}
private IPEndPoint EnsureLocalEndpoint(bool replace)
{
if (LocalEndPoint != null)
{
if (replace)
{
_proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
}
else
{
return (IPEndPoint)LocalEndPoint;
}
}
IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType));
LocalEndPoint = localEp;
return localEp;
}
public LdnProxySocket AsAccepted(IPEndPoint remoteEp)
{
Connected = true;
RemoteEndPoint = remoteEp;
IPEndPoint localEp = EnsureLocalEndpoint(true);
_proxy.SignalConnected(localEp, remoteEp, ProtocolType);
return this;
}
private void SignalError(WsaError error)
{
lock (_errors)
{
_errors.Enqueue((int)error);
}
}
private IPEndPoint GetEndpoint(uint ipv4, ushort port)
{
byte[] address = BitConverter.GetBytes(ipv4);
Array.Reverse(address);
return new IPEndPoint(new IPAddress(address), port);
}
public void IncomingData(ProxyDataPacket packet)
{
bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4);
if (!_closed && (_broadcast || !isBroadcast))
{
lock (_receiveQueue)
{
_receiveQueue.Enqueue(packet);
}
}
}
public ISocketImpl Accept()
{
if (!_isListening)
{
throw new InvalidOperationException();
}
// Accept a pending request to this socket.
lock (_connectRequests)
{
if (!Blocking && _connectRequests.Count == 0)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
while (true)
{
_acceptEvent.WaitOne(_acceptTimeout);
lock (_connectRequests)
{
while (_connectRequests.Count > 0)
{
ProxyConnectRequest request = _connectRequests.Dequeue();
if (_connectRequests.Count > 0)
{
_acceptEvent.Set(); // Still more accepts to do.
}
// Is this request made for us?
IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort);
if (Equals(endpoint, LocalEndPoint))
{
// Yes - let's accept.
IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort);
LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint);
lock (_listenSockets)
{
_listenSockets.Add(socket);
}
return socket;
}
}
}
}
}
public void Bind(EndPoint localEP)
{
ArgumentNullException.ThrowIfNull(localEP);
if (LocalEndPoint != null)
{
_proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
}
var asIPEndpoint = (IPEndPoint)localEP;
if (asIPEndpoint.Port == 0)
{
asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType);
}
LocalEndPoint = (IPEndPoint)localEP;
IsBound = true;
}
public void Close()
{
_closed = true;
_proxy.UnregisterSocket(this);
if (Connected)
{
Disconnect(false);
}
lock (_listenSockets)
{
foreach (LdnProxySocket socket in _listenSockets)
{
socket.Close();
}
}
_isListening = false;
}
public void Connect(EndPoint remoteEP)
{
if (_isListening || !IsBound)
{
throw new InvalidOperationException();
}
if (remoteEP is not IPEndPoint)
{
throw new NotSupportedException();
}
IPEndPoint localEp = EnsureLocalEndpoint(true);
_connecting = true;
_proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType);
if (!Blocking && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
_connectEvent.WaitOne(); //timeout?
if (_connectResponse.Info.SourceIpV4 == 0)
{
throw new SocketException((int)WsaError.WSAECONNREFUSED);
}
_connectResponse = default;
}
public void HandleConnectResponse(ProxyConnectResponse obj)
{
if (!_connecting)
{
return;
}
_connecting = false;
if (_connectResponse.Info.SourceIpV4 != 0)
{
IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort);
RemoteEndPoint = remoteEp;
Connected = true;
}
else
{
// Connection failed
SignalError(WsaError.WSAECONNREFUSED);
}
}
public void Disconnect(bool reuseSocket)
{
if (Connected)
{
ConnectionEnded();
// The other side needs to be notified that connection ended.
_proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType);
}
}
private void ConnectionEnded()
{
if (Connected)
{
RemoteEndPoint = null;
Connected = false;
}
}
public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue)
{
if (optionLevel != SocketOptionLevel.Socket)
{
throw new NotImplementedException();
}
if (_socketOptions.TryGetValue(optionName, out int result))
{
byte[] data = BitConverter.GetBytes(result);
Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length));
}
else
{
throw new NotImplementedException();
}
}
public void Listen(int backlog)
{
if (!IsBound)
{
throw new SocketException();
}
_isListening = true;
}
public void HandleConnectRequest(ProxyConnectRequest obj)
{
lock (_connectRequests)
{
_connectRequests.Enqueue(obj);
}
_connectEvent.Set();
}
public void HandleDisconnect(ProxyDisconnectMessage message)
{
Disconnect(false);
}
public int Receive(Span<byte> buffer)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, SocketFlags.None, ref dummy);
}
public int Receive(Span<byte> buffer, SocketFlags flags)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, flags, ref dummy);
}
public int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, flags, out socketError, ref dummy);
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEp)
{
// We just receive all packets meant for us anyways regardless of EP in the actual implementation.
// The point is mostly to return the endpoint that we got the data from.
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, ref remoteEp);
}
else if (_readShutdown)
{
return 0;
}
else if (!Blocking)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
int timeout = _receiveTimeout;
_receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, ref remoteEp);
}
else if (_readShutdown)
{
return 0;
}
else
{
throw new SocketException((int)WsaError.WSAETIMEDOUT);
}
}
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
{
// We just receive all packets meant for us anyways regardless of EP in the actual implementation.
// The point is mostly to return the endpoint that we got the data from.
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
socketError = SocketError.ConnectionReset;
return -1;
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
}
else if (_readShutdown)
{
socketError = SocketError.Success;
return 0;
}
else if (!Blocking)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
int timeout = _receiveTimeout;
_receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
}
else if (_readShutdown)
{
socketError = SocketError.Success;
return 0;
}
else
{
socketError = SocketError.TimedOut;
return -1;
}
}
}
private int ReceiveFromQueue(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEp)
{
int size = buffer.Length;
// Assumes we have the receive queue lock, and at least one item in the queue.
ProxyDataPacket packet = _receiveQueue.Peek();
remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
bool peek = (flags & SocketFlags.Peek) != 0;
int read;
if (packet.Data.Length > size)
{
read = size;
// Cannot fit in the output buffer. Copy up to what we've got.
packet.Data.AsSpan(0, size).CopyTo(buffer);
if (ProtocolType == ProtocolType.Udp)
{
// Udp overflows, loses the data, then throws an exception.
if (!peek)
{
_receiveQueue.Dequeue();
}
throw new SocketException((int)WsaError.WSAEMSGSIZE);
}
else if (ProtocolType == ProtocolType.Tcp)
{
// Split the data at the buffer boundary. It will stay on the recieve queue.
byte[] newData = new byte[packet.Data.Length - size];
Array.Copy(packet.Data, size, newData, 0, newData.Length);
packet.Data = newData;
}
}
else
{
read = packet.Data.Length;
packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
if (!peek)
{
_receiveQueue.Dequeue();
}
}
return read;
}
private int ReceiveFromQueue(Span<byte> buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
{
int size = buffer.Length;
// Assumes we have the receive queue lock, and at least one item in the queue.
ProxyDataPacket packet = _receiveQueue.Peek();
remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
bool peek = (flags & SocketFlags.Peek) != 0;
int read;
if (packet.Data.Length > size)
{
read = size;
// Cannot fit in the output buffer. Copy up to what we've got.
packet.Data.AsSpan(0, size).CopyTo(buffer);
if (ProtocolType == ProtocolType.Udp)
{
// Udp overflows, loses the data, then throws an exception.
if (!peek)
{
_receiveQueue.Dequeue();
}
socketError = SocketError.MessageSize;
return -1;
}
else if (ProtocolType == ProtocolType.Tcp)
{
// Split the data at the buffer boundary. It will stay on the recieve queue.
byte[] newData = new byte[packet.Data.Length - size];
Array.Copy(packet.Data, size, newData, 0, newData.Length);
packet.Data = newData;
}
}
else
{
read = packet.Data.Length;
packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
if (!peek)
{
_receiveQueue.Dequeue();
}
}
socketError = SocketError.Success;
return read;
}
public int Send(ReadOnlySpan<byte> buffer)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, SocketFlags.None, RemoteEndPoint);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, flags, RemoteEndPoint);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, flags, out socketError, RemoteEndPoint);
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, EndPoint remoteEP)
{
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
IPEndPoint localEp = EnsureLocalEndpoint(false);
if (remoteEP is not IPEndPoint)
{
throw new NotSupportedException();
}
return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP)
{
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
socketError = SocketError.ConnectionReset;
return -1;
}
IPEndPoint localEp = EnsureLocalEndpoint(false);
if (remoteEP is not IPEndPoint)
{
// throw new NotSupportedException();
socketError = SocketError.OperationNotSupported;
return -1;
}
socketError = SocketError.Success;
return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
}
public bool Poll(int microSeconds, SelectMode mode)
{
return mode switch
{
SelectMode.SelectRead => Readable,
SelectMode.SelectWrite => Writable,
SelectMode.SelectError => Error,
_ => false
};
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
{
if (optionLevel != SocketOptionLevel.Socket)
{
throw new NotImplementedException();
}
switch (optionName)
{
case SocketOptionName.SendTimeout:
//_sendTimeout = optionValue;
break;
case SocketOptionName.ReceiveTimeout:
_receiveTimeout = optionValue;
break;
case SocketOptionName.Broadcast:
_broadcast = optionValue != 0;
break;
}
lock (_socketOptions)
{
_socketOptions[optionName] = optionValue;
}
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue)
{
// Just linger uses this for now in BSD, which we ignore.
}
public void Shutdown(SocketShutdown how)
{
switch (how)
{
case SocketShutdown.Both:
_readShutdown = true;
// _writeShutdown = true;
break;
case SocketShutdown.Receive:
_readShutdown = true;
break;
case SocketShutdown.Send:
// _writeShutdown = true;
break;
}
}
public void ProxyDestroyed()
{
// Do nothing, for now. Will likely be more useful with TCP.
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,93 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using System.Net.Sockets;
using System.Threading;
using TcpClient = NetCoreServer.TcpClient;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class P2pProxyClient : TcpClient, IProxyClient
{
private const int FailureTimeout = 4000;
public ProxyConfig ProxyConfig { get; private set; }
private readonly RyuLdnProtocol _protocol;
private readonly ManualResetEvent _connected = new ManualResetEvent(false);
private readonly ManualResetEvent _ready = new ManualResetEvent(false);
private readonly AutoResetEvent _error = new AutoResetEvent(false);
public P2pProxyClient(string address, int port) : base(address, port)
{
if (ProxyHelpers.SupportsNoDelay())
{
OptionNoDelay = true;
}
_protocol = new RyuLdnProtocol();
_protocol.ProxyConfig += HandleProxyConfig;
ConnectAsync();
}
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}");
_connected.Set();
}
protected override void OnDisconnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}");
SocketHelpers.UnregisterProxy();
_connected.Reset();
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
_protocol.Read(buffer, (int)offset, (int)size);
}
protected override void OnError(SocketError error)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}");
_error.Set();
}
private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
{
ProxyConfig = config;
SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
_ready.Set();
}
public bool EnsureProxyReady()
{
return _ready.WaitOne(FailureTimeout);
}
public bool PerformAuth(ExternalProxyConfig config)
{
bool signalled = _connected.WaitOne(FailureTimeout);
if (!signalled)
{
return false;
}
SendAsync(_protocol.Encode(PacketId.ExternalProxy, config));
return true;
}
}
}

View File

@ -0,0 +1,388 @@
using NetCoreServer;
using Open.Nat;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class P2pProxyServer : TcpServer, IDisposable
{
public const ushort PrivatePortBase = 39990;
public const int PrivatePortRange = 10;
private const ushort PublicPortBase = 39990;
private const int PublicPortRange = 10;
private const ushort PortLeaseLength = 60;
private const ushort PortLeaseRenew = 50;
private const ushort AuthWaitSeconds = 1;
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
public ushort PrivatePort { get; }
private ushort _publicPort;
private bool _disposed;
private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource();
private NatDevice _natDevice;
private Mapping _portMapping;
private readonly List<P2pProxySession> _players = new List<P2pProxySession>();
private readonly List<ExternalProxyToken> _waitingTokens = new List<ExternalProxyToken>();
private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false);
private uint _broadcastAddress;
private readonly LdnMasterProxyClient _master;
private readonly RyuLdnProtocol _masterProtocol;
private readonly RyuLdnProtocol _protocol;
public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port)
{
if (ProxyHelpers.SupportsNoDelay())
{
OptionNoDelay = true;
}
PrivatePort = port;
_master = master;
_masterProtocol = masterProtocol;
_masterProtocol.ExternalProxyState += HandleStateChange;
_masterProtocol.ExternalProxyToken += HandleToken;
_protocol = new RyuLdnProtocol();
}
private void HandleToken(LdnHeader header, ExternalProxyToken token)
{
_lock.EnterWriteLock();
_waitingTokens.Add(token);
_lock.ExitWriteLock();
_tokenEvent.Set();
}
private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state)
{
if (!state.Connected)
{
_lock.EnterWriteLock();
_waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress);
_players.RemoveAll(player =>
{
if (player.VirtualIpAddress == state.IpAddress)
{
player.DisconnectAndStop();
return true;
}
return false;
});
_lock.ExitWriteLock();
}
}
public void Configure(ProxyConfig config)
{
_broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask);
}
public async Task<ushort> NatPunch()
{
NatDiscoverer discoverer = new NatDiscoverer();
CancellationTokenSource cts = new CancellationTokenSource(1000);
NatDevice device;
try
{
device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts);
}
catch (NatDeviceNotFoundException)
{
return 0;
}
_publicPort = PublicPortBase;
for (int i = 0; i < PublicPortRange; i++)
{
try
{
_portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer");
await device.CreatePortMapAsync(_portMapping);
break;
}
catch (MappingException)
{
_publicPort++;
}
catch (Exception)
{
return 0;
}
if (i == PublicPortRange - 1)
{
_publicPort = 0;
}
}
if (_publicPort != 0)
{
_ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
}
_natDevice = device;
return _publicPort;
}
// Proxy handlers
private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action<P2pProxySession> action)
{
if (info.SourceIpV4 == 0)
{
// If they sent from a connection bound on 0.0.0.0, make others see it as them.
info.SourceIpV4 = sender.VirtualIpAddress;
}
else if (info.SourceIpV4 != sender.VirtualIpAddress)
{
// Can't pretend to be somebody else.
return;
}
uint destIp = info.DestIpV4;
if (destIp == 0xc0a800ff)
{
destIp = _broadcastAddress;
}
bool isBroadcast = destIp == _broadcastAddress;
_lock.EnterReadLock();
if (isBroadcast)
{
_players.ForEach(player =>
{
action(player);
});
}
else
{
P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp);
if (target != null)
{
action(target);
}
}
_lock.ExitReadLock();
}
public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message)
{
RouteMessage(sender, ref message.Info, (target) =>
{
target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message));
});
}
public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data)
{
RouteMessage(sender, ref message.Info, (target) =>
{
target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data));
});
}
public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message)
{
RouteMessage(sender, ref message.Info, (target) =>
{
target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message));
});
}
public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message)
{
RouteMessage(sender, ref message.Info, (target) =>
{
target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message));
});
}
// End proxy handlers
private async Task RefreshLease()
{
if (_disposed || _natDevice == null)
{
return;
}
try
{
await _natDevice.CreatePortMapAsync(_portMapping);
}
catch (Exception)
{
}
_ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
}
public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config)
{
_lock.EnterWriteLock();
// Attempt to find matching configuration. If we don't find one, wait for a bit and try again.
// Woken by new tokens coming in from the master server.
IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address;
byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address);
long time;
long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds;
do
{
for (int i = 0; i < _waitingTokens.Count; i++)
{
ExternalProxyToken waitToken = _waitingTokens[i];
// Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token)
bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]);
bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes);
if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan()))
{
// This is a match.
_waitingTokens.RemoveAt(i);
session.SetIpv4(waitToken.VirtualIp);
ProxyConfig pconfig = new ProxyConfig
{
ProxyIp = session.VirtualIpAddress,
ProxySubnetMask = 0xFFFF0000 // TODO: Use from server.
};
if (_players.Count == 0)
{
Configure(pconfig);
}
_players.Add(session);
session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig));
_lock.ExitWriteLock();
return true;
}
}
// Couldn't find the token.
// It may not have arrived yet, so wait for one to arrive.
_lock.ExitWriteLock();
time = Stopwatch.GetTimestamp();
int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000));
if (remainingMs < 0)
{
remainingMs = 0;
}
_tokenEvent.WaitOne(remainingMs);
_lock.EnterWriteLock();
} while (time < endTime);
_lock.ExitWriteLock();
return false;
}
public void DisconnectProxyClient(P2pProxySession session)
{
_lock.EnterWriteLock();
bool removed = _players.Remove(session);
if (removed)
{
_master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState
{
IpAddress = session.VirtualIpAddress,
Connected = false
}));
}
_lock.ExitWriteLock();
}
public new void Dispose()
{
base.Dispose();
_disposed = true;
_disposedCancellation.Cancel();
try
{
Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer"));
// Just absorb any exceptions.
delete?.ContinueWith((task) => { });
}
catch (Exception)
{
// Fail silently.
}
}
protected override TcpSession CreateSession()
{
return new P2pProxySession(this);
}
protected override void OnError(SocketError error)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}");
}
}
}

View File

@ -0,0 +1,90 @@
using NetCoreServer;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class P2pProxySession : TcpSession
{
public uint VirtualIpAddress { get; private set; }
public RyuLdnProtocol Protocol { get; }
private readonly P2pProxyServer _parent;
private bool _masterClosed;
public P2pProxySession(P2pProxyServer server) : base(server)
{
_parent = server;
Protocol = new RyuLdnProtocol();
Protocol.ProxyDisconnect += HandleProxyDisconnect;
Protocol.ProxyData += HandleProxyData;
Protocol.ProxyConnectReply += HandleProxyConnectReply;
Protocol.ProxyConnect += HandleProxyConnect;
Protocol.ExternalProxy += HandleAuthentication;
}
private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token)
{
if (!_parent.TryRegisterUser(this, token))
{
Disconnect();
}
}
public void SetIpv4(uint ip)
{
VirtualIpAddress = ip;
}
public void DisconnectAndStop()
{
_masterClosed = true;
Disconnect();
}
protected override void OnDisconnected()
{
if (!_masterClosed)
{
_parent.DisconnectProxyClient(this);
}
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
try
{
Protocol.Read(buffer, (int)offset, (int)size);
}
catch (Exception)
{
Disconnect();
}
}
private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message)
{
_parent.HandleProxyDisconnect(this, header, message);
}
private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data)
{
_parent.HandleProxyData(this, header, message, data);
}
private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data)
{
_parent.HandleProxyConnectReply(this, header, data);
}
private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message)
{
_parent.HandleProxyConnect(this, header, message);
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
static class ProxyHelpers
{
public static byte[] AddressTo16Byte(IPAddress address)
{
byte[] ipBytes = new byte[16];
byte[] srcBytes = address.GetAddressBytes();
Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length);
return ipBytes;
}
public static bool SupportsNoDelay()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
}
}

View File

@ -0,0 +1,380 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
class RyuLdnProtocol
{
private const byte CurrentProtocolVersion = 1;
private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24);
private const int MaxPacketSize = 131072;
private readonly int _headerSize = Marshal.SizeOf<LdnHeader>();
private readonly byte[] _buffer = new byte[MaxPacketSize];
private int _bufferEnd = 0;
// Client Packets.
public event Action<LdnHeader, InitializeMessage> Initialize;
public event Action<LdnHeader, PassphraseMessage> Passphrase;
public event Action<LdnHeader, NetworkInfo> Connected;
public event Action<LdnHeader, NetworkInfo> SyncNetwork;
public event Action<LdnHeader, NetworkInfo> ScanReply;
public event Action<LdnHeader> ScanReplyEnd;
public event Action<LdnHeader, DisconnectMessage> Disconnected;
// External Proxy Packets.
public event Action<LdnHeader, ExternalProxyConfig> ExternalProxy;
public event Action<LdnHeader, ExternalProxyConnectionState> ExternalProxyState;
public event Action<LdnHeader, ExternalProxyToken> ExternalProxyToken;
// Server Packets.
public event Action<LdnHeader, CreateAccessPointRequest, byte[]> CreateAccessPoint;
public event Action<LdnHeader, CreateAccessPointPrivateRequest, byte[]> CreateAccessPointPrivate;
public event Action<LdnHeader, RejectRequest> Reject;
public event Action<LdnHeader> RejectReply;
public event Action<LdnHeader, SetAcceptPolicyRequest> SetAcceptPolicy;
public event Action<LdnHeader, byte[]> SetAdvertiseData;
public event Action<LdnHeader, ConnectRequest> Connect;
public event Action<LdnHeader, ConnectPrivateRequest> ConnectPrivate;
public event Action<LdnHeader, ScanFilter> Scan;
// Proxy Packets.
public event Action<LdnHeader, ProxyConfig> ProxyConfig;
public event Action<LdnHeader, ProxyConnectRequest> ProxyConnect;
public event Action<LdnHeader, ProxyConnectResponse> ProxyConnectReply;
public event Action<LdnHeader, ProxyDataHeader, byte[]> ProxyData;
public event Action<LdnHeader, ProxyDisconnectMessage> ProxyDisconnect;
// Lifecycle Packets.
public event Action<LdnHeader, NetworkErrorMessage> NetworkError;
public event Action<LdnHeader, PingMessage> Ping;
public RyuLdnProtocol() { }
public void Reset()
{
_bufferEnd = 0;
}
public void Read(byte[] data, int offset, int size)
{
int index = 0;
while (index < size)
{
if (_bufferEnd < _headerSize)
{
// Assemble the header first.
int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd));
Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
index += copyable;
_bufferEnd += copyable;
}
if (_bufferEnd >= _headerSize)
{
// The header is available. Make sure we received all the data (size specified in the header)
LdnHeader ldnHeader = MemoryMarshal.Cast<byte, LdnHeader>(_buffer)[0];
if (ldnHeader.Magic != Magic)
{
throw new InvalidOperationException("Invalid magic number in received packet.");
}
if (ldnHeader.Version != CurrentProtocolVersion)
{
throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}.");
}
int finalSize = _headerSize + ldnHeader.DataSize;
if (finalSize >= MaxPacketSize)
{
throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded.");
}
int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd));
Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
index += copyable;
_bufferEnd += copyable;
if (finalSize == _bufferEnd)
{
// The full packet has been retrieved. Send it to be decoded.
byte[] ldnData = new byte[ldnHeader.DataSize];
Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length);
DecodeAndHandle(ldnHeader, ldnData);
Reset();
}
}
}
}
private (T, byte[]) ParseWithData<T>(byte[] data) where T : struct
{
T str = default;
int size = Marshal.SizeOf(str);
byte[] remainder = new byte[data.Length - size];
if (remainder.Length > 0)
{
Array.Copy(data, size, remainder, 0, remainder.Length);
}
return (MemoryMarshal.Read<T>(data), remainder);
}
private void DecodeAndHandle(LdnHeader header, byte[] data)
{
switch ((PacketId)header.Type)
{
// Client Packets.
case PacketId.Initialize:
{
Initialize?.Invoke(header, MemoryMarshal.Read<InitializeMessage>(data));
break;
}
case PacketId.Passphrase:
{
Passphrase?.Invoke(header, MemoryMarshal.Read<PassphraseMessage>(data));
break;
}
case PacketId.Connected:
{
Connected?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.SyncNetwork:
{
SyncNetwork?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReply:
{
ScanReply?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReplyEnd:
{
ScanReplyEnd?.Invoke(header);
break;
}
case PacketId.Disconnect:
{
Disconnected?.Invoke(header, MemoryMarshal.Read<DisconnectMessage>(data));
break;
}
// External Proxy Packets.
case PacketId.ExternalProxy:
{
ExternalProxy?.Invoke(header, MemoryMarshal.Read<ExternalProxyConfig>(data));
break;
}
case PacketId.ExternalProxyState:
{
ExternalProxyState?.Invoke(header, MemoryMarshal.Read<ExternalProxyConnectionState>(data));
break;
}
case PacketId.ExternalProxyToken:
{
ExternalProxyToken?.Invoke(header, MemoryMarshal.Read<ExternalProxyToken>(data));
break;
}
// Server Packets.
case PacketId.CreateAccessPoint:
{
(CreateAccessPointRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointRequest>(data);
CreateAccessPoint?.Invoke(header, packet, extraData);
break;
}
case PacketId.CreateAccessPointPrivate:
{
(CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointPrivateRequest>(data);
CreateAccessPointPrivate?.Invoke(header, packet, extraData);
break;
}
case PacketId.Reject:
{
Reject?.Invoke(header, MemoryMarshal.Read<RejectRequest>(data));
break;
}
case PacketId.RejectReply:
{
RejectReply?.Invoke(header);
break;
}
case PacketId.SetAcceptPolicy:
{
SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read<SetAcceptPolicyRequest>(data));
break;
}
case PacketId.SetAdvertiseData:
{
SetAdvertiseData?.Invoke(header, data);
break;
}
case PacketId.Connect:
{
Connect?.Invoke(header, MemoryMarshal.Read<ConnectRequest>(data));
break;
}
case PacketId.ConnectPrivate:
{
ConnectPrivate?.Invoke(header, MemoryMarshal.Read<ConnectPrivateRequest>(data));
break;
}
case PacketId.Scan:
{
Scan?.Invoke(header, MemoryMarshal.Read<ScanFilter>(data));
break;
}
// Proxy Packets
case PacketId.ProxyConfig:
{
ProxyConfig?.Invoke(header, MemoryMarshal.Read<ProxyConfig>(data));
break;
}
case PacketId.ProxyConnect:
{
ProxyConnect?.Invoke(header, MemoryMarshal.Read<ProxyConnectRequest>(data));
break;
}
case PacketId.ProxyConnectReply:
{
ProxyConnectReply?.Invoke(header, MemoryMarshal.Read<ProxyConnectResponse>(data));
break;
}
case PacketId.ProxyData:
{
(ProxyDataHeader packet, byte[] extraData) = ParseWithData<ProxyDataHeader>(data);
ProxyData?.Invoke(header, packet, extraData);
break;
}
case PacketId.ProxyDisconnect:
{
ProxyDisconnect?.Invoke(header, MemoryMarshal.Read<ProxyDisconnectMessage>(data));
break;
}
// Lifecycle Packets.
case PacketId.Ping:
{
Ping?.Invoke(header, MemoryMarshal.Read<PingMessage>(data));
break;
}
case PacketId.NetworkError:
{
NetworkError?.Invoke(header, MemoryMarshal.Read<NetworkErrorMessage>(data));
break;
}
default:
break;
}
}
private static LdnHeader GetHeader(PacketId type, int dataSize)
{
return new LdnHeader()
{
Magic = Magic,
Version = CurrentProtocolVersion,
Type = (byte)type,
DataSize = dataSize
};
}
public byte[] Encode(PacketId type)
{
LdnHeader header = GetHeader(type, 0);
return SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
}
public byte[] Encode(PacketId type, byte[] data)
{
LdnHeader header = GetHeader(type, data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + data.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>(), data.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet, byte[] data) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length + data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length + data.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>() + packetData.Length, data.Length);
return result;
}
}
}

View File

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
struct DisconnectMessage
{
public uint DisconnectIP;
}
}

View File

@ -0,0 +1,19 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Sent by the server to point a client towards an external server being used as a proxy.
/// The client then forwards this to the external proxy after connecting, to verify the connection worked.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)]
struct ExternalProxyConfig
{
public Array16<byte> ProxyIp;
public AddressFamily AddressFamily;
public ushort ProxyPort;
public Array16<byte> Token;
}
}

View File

@ -0,0 +1,18 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Indicates a change in connection state for the given client.
/// Is sent to notify the master server when connection is first established.
/// Can be sent by the external proxy to the master server to notify it of a proxy disconnect.
/// Can be sent by the master server to notify the external proxy of a user leaving a room.
/// Both will result in a force kick.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)]
struct ExternalProxyConnectionState
{
public uint IpAddress;
public bool Connected;
}
}

View File

@ -0,0 +1,20 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Sent by the master server to an external proxy to tell them someone is going to connect.
/// This drives authentication, and lets the proxy know what virtual IP to give to each joiner,
/// as these are managed by the master server.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
struct ExternalProxyToken
{
public uint VirtualIp;
public Array16<byte> Token;
public Array16<byte> PhysicalIp;
public AddressFamily AddressFamily;
}
}

View File

@ -0,0 +1,20 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// This message is first sent by the client to identify themselves.
/// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id)
/// Otherwise, they are returned a random mac address.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x16)]
struct InitializeMessage
{
// All 0 if we don't have an ID yet.
public Array16<byte> Id;
// All 0 if we don't have a mac yet.
public Array6<byte> MacAddress;
}
}

View File

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0xA)]
struct LdnHeader
{
public uint Magic;
public byte Type;
public byte Version;
public int DataSize;
}
}

View File

@ -0,0 +1,36 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
enum PacketId
{
Initialize,
Passphrase,
CreateAccessPoint,
CreateAccessPointPrivate,
ExternalProxy,
ExternalProxyToken,
ExternalProxyState,
SyncNetwork,
Reject,
RejectReply,
Scan,
ScanReply,
ScanReplyEnd,
Connect,
ConnectPrivate,
Connected,
Disconnect,
ProxyConfig,
ProxyConnect,
ProxyConnectReply,
ProxyData,
ProxyDisconnect,
SetAcceptPolicy,
SetAdvertiseData,
Ping = 254,
NetworkError = 255
}
}

View File

@ -0,0 +1,11 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x80)]
struct PassphraseMessage
{
public Array128<byte> Passphrase;
}
}

View File

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x2)]
struct PingMessage
{
public byte Requester;
public byte Id;
}
}

View File

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct ProxyConnectRequest
{
public ProxyInfo Info;
}
}

View File

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct ProxyConnectResponse
{
public ProxyInfo Info;
}
}

View File

@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Represents data sent over a transport layer.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
struct ProxyDataHeader
{
public ProxyInfo Info;
public uint DataLength; // Followed by the data with the specified byte length.
}
}

View File

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
class ProxyDataPacket
{
public ProxyDataHeader Header;
public byte[] Data;
}
}

View File

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
struct ProxyDisconnectMessage
{
public ProxyInfo Info;
public int DisconnectReason;
}
}

View File

@ -0,0 +1,20 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Information included in all proxied communication.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)]
struct ProxyInfo
{
public uint SourceIpV4;
public ushort SourcePort;
public uint DestIpV4;
public ushort DestPort;
public ProtocolType Protocol;
}
}

View File

@ -0,0 +1,18 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x8)]
struct RejectRequest
{
public uint NodeId;
public DisconnectReason DisconnectReason;
public RejectRequest(DisconnectReason disconnectReason, uint nodeId)
{
DisconnectReason = disconnectReason;
NodeId = nodeId;
}
}
}

View File

@ -0,0 +1,23 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)]
struct RyuNetworkConfig
{
public Array16<byte> GameVersion;
// PrivateIp is included for external proxies for the case where a client attempts to join from
// their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP,
// so if their public IP is identical, the internal address should be sent instead.
// The fields below are 0 if not hosting a p2p proxy.
public Array16<byte> PrivateIp;
public AddressFamily AddressFamily;
public ushort ExternalProxyPort;
public ushort InternalProxyPort;
}
}

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