diff --git a/.editorconfig b/.editorconfig
index 9d695c7fb..db08c67e2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -233,6 +233,29 @@ dotnet_naming_style.IPascalCase.required_suffix =
dotnet_naming_style.IPascalCase.word_separator =
dotnet_naming_style.IPascalCase.capitalization = pascal_case
+# TODO:
+# .NET 8 migration (new warnings are caused by the NET 8 C# compiler and analyzer)
+# The following info messages might need to be fixed in the source code instead of hiding the actual message
+# Without the following lines, dotnet format would fail
+# Disable "Collection initialization can be simplified"
+dotnet_diagnostic.IDE0028.severity = none
+dotnet_diagnostic.IDE0300.severity = none
+dotnet_diagnostic.IDE0301.severity = none
+dotnet_diagnostic.IDE0302.severity = none
+dotnet_diagnostic.IDE0305.severity = none
+# Disable "'new' expression can be simplified"
+dotnet_diagnostic.IDE0090.severity = none
+# Disable "Use primary constructor"
+dotnet_diagnostic.IDE0290.severity = none
+# Disable "Member '' does not access instance data and can be marked as static"
+dotnet_diagnostic.CA1822.severity = none
+# Disable "Change type of field '' from '' to '' for improved performance"
+dotnet_diagnostic.CA1859.severity = none
+# Disable "Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array"
+dotnet_diagnostic.CA1861.severity = none
+# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'"
+dotnet_diagnostic.CA1862.severity = none
+
[src/Ryujinx.HLE/HOS/Services/**.cs]
# Disable "mark members as static" rule for services
dotnet_diagnostic.CA1822.severity = none
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 16058d9f8..7d46adc2c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -30,7 +30,7 @@ jobs:
- os: windows-latest
OS_NAME: Windows x64
- DOTNET_RUNTIME_IDENTIFIER: win10-x64
+ DOTNET_RUNTIME_IDENTIFIER: win-x64
RELEASE_ZIP_OS_NAME: win_x64
fail-fast: false
@@ -155,4 +155,4 @@ jobs:
with:
name: sdl2-ryujinx-headless-${{ 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'
\ No newline at end of file
+ if: github.event_name == 'pull_request'
diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index 4c8ba3e17..c1ae9fe8f 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -49,7 +49,9 @@ jobs:
run: python -m pip install PyYAML lxml
- name: Restore Nuget packages
- run: dotnet restore Ryujinx/${{ env.RYUJINX_PROJECT_FILE }}
+ # With .NET 8.0.100, Microsoft.NET.ILLink.Tasks isn't restored by default and only seems to appears when publishing.
+ # So we just publish to grab the dependencies
+ run: dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained
- name: Generate nuget_sources.json
shell: python
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 988264a31..4dc1d091d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ env:
jobs:
tag:
name: Create tag
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- name: Get version info
id: version_info
@@ -59,7 +59,7 @@ jobs:
- os: windows-latest
OS_NAME: Windows x64
- DOTNET_RUNTIME_IDENTIFIER: win10-x64
+ DOTNET_RUNTIME_IDENTIFIER: win-x64
RELEASE_ZIP_OS_NAME: win_x64
steps:
- uses: actions/checkout@v4
@@ -156,11 +156,11 @@ jobs:
with:
global-json-file: global.json
- - name: Setup LLVM 14
+ - name: Setup LLVM 15
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
- sudo ./llvm.sh 14
+ sudo ./llvm.sh 15
- name: Install rcodesign
run: |
@@ -215,4 +215,4 @@ jobs:
needs: release
with:
ryujinx_version: "1.1.${{ github.run_number }}"
- secrets: inherit
\ No newline at end of file
+ secrets: inherit
diff --git a/Directory.Packages.props b/Directory.Packages.props
index cde8742f9..44f9b5d40 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,13 +3,13 @@
true
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -18,23 +18,25 @@
-
+
-
+
+
-
-
-
-
+
+
+
+
+
@@ -43,10 +45,10 @@
-
-
-
-
+
+
+
+
diff --git a/README.md b/README.md
index 7021abc45..b2f95cc1f 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of
If you wish to build the emulator yourself, follow these steps:
### Step 1
-Install the X64 version of [.NET 7.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/7.0).
+Install the X64 version of [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
### Step 2
Either use `git clone https://github.com/Ryujinx/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files.
@@ -141,3 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
+- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
+- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md
index 4cc8b7a45..5caa03771 100644
--- a/distribution/legal/THIRDPARTY.md
+++ b/distribution/legal/THIRDPARTY.md
@@ -681,4 +681,33 @@
END OF TERMS AND CONDITIONS
```
-
\ No newline at end of file
+
+
+# ShellLink (MIT)
+
+ See License
+
+ ```
+ MIT License
+
+ Copyright (c) 2017 Yorick Koster, Securify B.V.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ ```
+
diff --git a/distribution/linux/Ryujinx.desktop b/distribution/linux/Ryujinx.desktop
index 19cc5d6cc..a4550d104 100644
--- a/distribution/linux/Ryujinx.desktop
+++ b/distribution/linux/Ryujinx.desktop
@@ -3,8 +3,8 @@ Version=1.0
Name=Ryujinx
Type=Application
Icon=Ryujinx
-Exec=env DOTNET_EnableAlternateStackCheck=1 Ryujinx %f
-Comment=A Nintendo Switch Emulator
+Exec=Ryujinx.sh %f
+Comment=Plays Nintendo Switch applications
GenericName=Nintendo Switch Emulator
Terminal=false
Categories=Game;Emulator;
diff --git a/distribution/linux/shortcut-template.desktop b/distribution/linux/shortcut-template.desktop
new file mode 100644
index 000000000..6bee0f8d1
--- /dev/null
+++ b/distribution/linux/shortcut-template.desktop
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Version=1.0
+Name={0}
+Type=Application
+Icon={1}
+Exec={2} %f
+Comment=Nintendo Switch application
+GenericName=Nintendo Switch Emulator
+Terminal=false
+Categories=Game;Emulator;
+Keywords=Switch;Nintendo;Emulator;
+StartupWMClass=Ryujinx
+PrefersNonDefaultGPU=true
diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist
index 6e068ba2d..51c71eaa1 100644
--- a/distribution/macos/Info.plist
+++ b/distribution/macos/Info.plist
@@ -43,7 +43,7 @@
LSApplicationCategoryType
public.app-category.games
LSMinimumSystemVersion
- 11.0
+ 12.0
UTExportedTypeDeclarations
@@ -155,4 +155,4 @@
200000
-
\ No newline at end of file
+
diff --git a/distribution/macos/shortcut-template.plist b/distribution/macos/shortcut-template.plist
new file mode 100644
index 000000000..27a9e46a9
--- /dev/null
+++ b/distribution/macos/shortcut-template.plist
@@ -0,0 +1,35 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ {0}
+ CFBundleGetInfoString
+ {1}
+ CFBundleIconFile
+ {2}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleVersion
+ 1.0
+ NSHighResolutionCapable
+
+ CSResourcesFileMapped
+
+ NSHumanReadableCopyright
+ Copyright © 2018 - 2023 Ryujinx Team and Contributors.
+ LSApplicationCategoryType
+ public.app-category.games
+ LSMinimumSystemVersion
+ 11.0
+ UIPrerenderedIcon
+
+ LSEnvironment
+
+ DOTNET_DefaultStackSize
+ 200000
+
+
+
diff --git a/global.json b/global.json
index 39ccef0d0..391ba3c2a 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.200",
+ "version": "8.0.100",
"rollForward": "latestFeature"
}
-}
\ No newline at end of file
+}
diff --git a/src/ARMeilleure/ARMeilleure.csproj b/src/ARMeilleure/ARMeilleure.csproj
index fa5551154..550e50c26 100644
--- a/src/ARMeilleure/ARMeilleure.csproj
+++ b/src/ARMeilleure/ARMeilleure.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/ARMeilleure/Decoders/Decoder.cs b/src/ARMeilleure/Decoders/Decoder.cs
index 6d07827a2..66d286928 100644
--- a/src/ARMeilleure/Decoders/Decoder.cs
+++ b/src/ARMeilleure/Decoders/Decoder.cs
@@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders
{
block = new Block(blkAddress);
- if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress))
+ if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) ||
+ opsCount > instructionLimit ||
+ (visited.Count > 0 && !memory.IsMapped(blkAddress)))
{
block.Exit = true;
block.EndAddress = blkAddress;
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index 91a054123..e2b5e2d10 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -117,12 +117,11 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
- bool result = TryFind(funcOffset, out CacheEntry entry);
- Debug.Assert(result);
-
- _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
-
- Remove(funcOffset);
+ if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
+ {
+ _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
+ _cacheEntries.RemoveAt(entryIndex);
+ }
}
}
@@ -181,22 +180,7 @@ namespace ARMeilleure.Translation.Cache
_cacheEntries.Insert(index, entry);
}
- private static void Remove(int offset)
- {
- int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
-
- if (index < 0)
- {
- index = ~index - 1;
- }
-
- if (index >= 0)
- {
- _cacheEntries.RemoveAt(index);
- }
- }
-
- public static bool TryFind(int offset, out CacheEntry entry)
+ public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
@@ -210,11 +194,13 @@ namespace ARMeilleure.Translation.Cache
if (index >= 0)
{
entry = _cacheEntries[index];
+ entryIndex = index;
return true;
}
}
entry = default;
+ entryIndex = 0;
return false;
}
}
diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
index 91fd19c25..3957a7559 100644
--- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
+++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs
@@ -95,7 +95,7 @@ namespace ARMeilleure.Translation.Cache
{
int offset = (int)((long)controlPc - context.ToInt64());
- if (!JitCache.TryFind(offset, out CacheEntry funcEntry))
+ if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _))
{
return null; // Not found.
}
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
index 115a37601..b5fd8f9e7 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
+++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
@@ -1,11 +1,11 @@
- net7.0
+ net8.0
-
+
diff --git a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
index 525f1f5b6..dd18e70a1 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
+++ b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
index 9f242dbe2..1d92d9d2e 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
+++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
@@ -1,9 +1,9 @@
- net7.0
+ net8.0
true
- win10-x64;linux-x64;osx-x64
+ win-x64;osx-x64;linux-x64
@@ -15,11 +15,11 @@
PreserveNewest
libsoundio.dll
-
+
PreserveNewest
libsoundio.dylib
-
+
PreserveNewest
libsoundio.so
diff --git a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs
index 4771ae4dd..abbb6ea6c 100644
--- a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs
@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Utils
throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null);
}
- MemoryMarshal.Write(backingMemory.Span[..size], ref data);
+ MemoryMarshal.Write(backingMemory.Span[..size], in data);
backingMemory = backingMemory[size..];
}
@@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Utils
throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null);
}
- MemoryMarshal.Write(backingMemory[..size], ref data);
+ MemoryMarshal.Write(backingMemory[..size], in data);
backingMemory = backingMemory[size..];
}
diff --git a/src/Ryujinx.Audio/Ryujinx.Audio.csproj b/src/Ryujinx.Audio/Ryujinx.Audio.csproj
index 4a159eb5c..fc20f4ec4 100644
--- a/src/Ryujinx.Audio/Ryujinx.Audio.csproj
+++ b/src/Ryujinx.Audio/Ryujinx.Audio.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index c473cf562..4d751e2a9 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -190,6 +190,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
+ ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
@@ -408,6 +409,11 @@ namespace Ryujinx.Ava
});
}
+ private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.EnableInternetAccess = e.NewValue;
+ }
+
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e)
{
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
@@ -710,7 +716,7 @@ namespace Ryujinx.Ava
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePreGame();
});
return true;
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index e392f762e..6f6f9c540 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -14,7 +14,7 @@
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
"MenuBarFileExit": "_Exit",
- "MenuBarOptions": "Options",
+ "MenuBarOptions": "_Options",
"MenuBarOptionsToggleFullscreen": "Toggle Fullscreen",
"MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode",
"MenuBarOptionsStopEmulation": "Stop Emulation",
@@ -30,7 +30,7 @@
"MenuBarToolsManageFileTypes": "Manage file types",
"MenuBarToolsInstallFileTypes": "Install file types",
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
- "MenuBarHelp": "Help",
+ "MenuBarHelp": "_Help",
"MenuBarHelpCheckForUpdates": "Check for Updates",
"MenuBarHelpAbout": "About",
"MenuSearch": "Search...",
@@ -72,6 +72,8 @@
"GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)",
"GameListContextMenuExtractDataLogo": "Logo",
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
+ "GameListContextMenuCreateShortcut": "Create Application Shortcut",
+ "GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
"StatusBarSystemVersion": "System Version: {0}",
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
@@ -650,7 +652,7 @@
"UserEditorTitle": "Edit User",
"UserEditorTitleCreate": "Create User",
"SettingsTabNetworkInterface": "Network Interface:",
- "NetworkInterfaceTooltip": "The network interface used for LAN features",
+ "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features",
"NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders",
"AboutChangelogButton": "View Changelog on GitHub",
diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
index b8cd06f3d..91ca8f4d5 100644
--- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs
+++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
@@ -173,7 +173,7 @@ namespace Ryujinx.Ava.Common
string extension = Path.GetExtension(titleFilePath).ToLower();
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{
- PartitionFileSystem pfs;
+ IFileSystem pfs;
if (extension == ".xci")
{
@@ -181,7 +181,9 @@ namespace Ryujinx.Ava.Common
}
else
{
- pfs = new PartitionFileSystem(file.AsStorage());
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
}
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs
index 168e9216d..cc062a256 100644
--- a/src/Ryujinx.Ava/Program.cs
+++ b/src/Ryujinx.Ava/Program.cs
@@ -6,13 +6,13 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Common.SystemInfo;
using System;
using System.IO;
using System.Runtime.InteropServices;
diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
index a4c1ebf16..6812e57c4 100644
--- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj
+++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
@@ -1,7 +1,7 @@
- net7.0
- win10-x64;osx-x64;linux-x64
+ net8.0
+ win-x64;osx-x64;linux-x64
Exe
true
1.0.0-dirty
@@ -25,6 +25,16 @@
partial
+
+
+ true
+
+
@@ -40,7 +50,7 @@
-
+
@@ -145,4 +155,4 @@
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
index 93638fc53..b8fe7e76f 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
@@ -12,6 +12,11 @@
Click="ToggleFavorite_Click"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
+
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
index d75572e65..0f0071065 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -337,6 +337,17 @@ namespace Ryujinx.Ava.UI.Controls
}
}
+ public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
+ {
+ var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
+
+ if (viewModel?.SelectedApplication != null)
+ {
+ ApplicationData selectedApplication = viewModel.SelectedApplication;
+ ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
+ }
+ }
+
public async void RunApplication_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
index 09011005b..9004f7518 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
@@ -126,17 +126,17 @@
Spacing="5">
diff --git a/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs
new file mode 100644
index 000000000..81d3bc303
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using System;
+
+namespace Ryujinx.Ava.UI.Controls
+{
+ public class SliderScroll : Slider
+ {
+ protected override Type StyleKeyOverride => typeof(Slider);
+
+ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+ {
+ var newValue = Value + e.Delta.Y * TickFrequency;
+
+ if (newValue < Minimum)
+ {
+ Value = Minimum;
+ }
+ else if (newValue > Maximum)
+ {
+ Value = Maximum;
+ }
+ else
+ {
+ Value = newValue;
+ }
+
+ e.Handled = true;
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
new file mode 100644
index 000000000..737896986
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
@@ -0,0 +1,43 @@
+using Avalonia.Data.Converters;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ui.Common.Helper;
+using System;
+using System.Globalization;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ ///
+ /// This makes sure that the string "Never" that's returned by is properly localized in the Avalonia UI.
+ /// After the Avalonia UI has been made the default and the GTK UI is removed, should be updated to directly return a localized string.
+ ///
+ internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
+ {
+ private static readonly LocalizedNeverConverter _instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not string valStr)
+ {
+ return "";
+ }
+
+ if (valStr == "Never")
+ {
+ return LocaleManager.Instance[LocaleKeys.Never];
+ }
+
+ return valStr;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return _instance;
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
deleted file mode 100644
index e91937612..000000000
--- a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Avalonia.Data.Converters;
-using Avalonia.Markup.Xaml;
-using Ryujinx.Ava.Common.Locale;
-using System;
-using System.Globalization;
-
-namespace Ryujinx.Ava.UI.Helpers
-{
- internal class NullableDateTimeConverter : MarkupExtension, IValueConverter
- {
- private static readonly NullableDateTimeConverter _instance = new();
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value == null)
- {
- return LocaleManager.Instance[LocaleKeys.Never];
- }
-
- if (value is DateTime dateTime)
- {
- return dateTime.ToLocalTime().ToString(culture);
- }
-
- throw new NotSupportedException();
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotSupportedException();
- }
-
- public override object ProvideValue(IServiceProvider serviceProvider)
- {
- return _instance;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
index ca55d0399..35d16b9e0 100644
--- a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
+++ b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
@@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Helpers
public static partial IntPtr SetCursor(IntPtr handle);
[LibraryImport("user32.dll")]
- public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvAndPlane, byte[] pvXorPlane);
+ public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, [In] byte[] pvAndPlane, [In] byte[] pvXorPlane);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
public static partial ushort RegisterClassEx(ref WndClassEx param);
diff --git a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
index 8a4346556..8340d39df 100644
--- a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
+++ b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
public int Compare(ApplicationData x, ApplicationData y)
{
- var aValue = x.LastPlayed;
- var bValue = y.LastPlayed;
+ DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
- if (!aValue.HasValue)
+ if (x?.LastPlayed != null)
{
- aValue = DateTime.UnixEpoch;
+ aValue = x.LastPlayed.Value;
}
- if (!bValue.HasValue)
+ if (y?.LastPlayed != null)
{
- bValue = DateTime.UnixEpoch;
+ bValue = y.LastPlayed.Value;
}
- return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value);
+ return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue);
}
}
}
diff --git a/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
new file mode 100644
index 000000000..d53ff566f
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
@@ -0,0 +1,31 @@
+using Ryujinx.Ui.App.Common;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.UI.Models.Generic
+{
+ internal class TimePlayedSortComparer : IComparer
+ {
+ public TimePlayedSortComparer() { }
+ public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
+
+ public bool IsAscending { get; }
+
+ public int Compare(ApplicationData x, ApplicationData y)
+ {
+ TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero;
+
+ if (x?.TimePlayed != null)
+ {
+ aValue = x.TimePlayed;
+ }
+
+ if (y?.TimePlayed != null)
+ {
+ bValue = y.TimePlayed;
+ }
+
+ return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue);
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs
index f15befbb3..7b476932b 100644
--- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs
+++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs
@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
-using System;
+using Ryujinx.Ui.Common.Helper;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
public bool SizeAvailable { get; set; }
- public string SizeString => GetSizeString();
-
- private string GetSizeString()
- {
- const int Scale = 1024;
- string[] orders = { "GiB", "MiB", "KiB" };
- long max = (long)Math.Pow(Scale, orders.Length);
-
- foreach (string order in orders)
- {
- if (Size > max)
- {
- return $"{decimal.Divide(Size, max):##.##} {order}";
- }
-
- max /= Scale;
- }
-
- return "0 KiB";
- }
+ public string SizeString => ValueFormatUtils.FormatFileSize(Size);
public SaveModel(SaveDataInfo info)
{
diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
index b88bd3d9c..cdecae77d 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -126,7 +126,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
- PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+ PartitionFileSystem partitionFileSystem = new();
+ partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(partitionFileSystem);
@@ -232,7 +233,8 @@ namespace Ryujinx.Ava.UI.ViewModels
using FileStream containerFile = File.OpenRead(path);
- PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+ PartitionFileSystem partitionFileSystem = new();
+ partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDownloadableContent = false;
_virtualFileSystem.ImportTickets(partitionFileSystem);
diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index 7a9e4df14..80df5d398 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -356,6 +356,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+ public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild();
+
public string LoadHeading
{
get => _loadHeading;
@@ -928,21 +930,20 @@ namespace Ryujinx.Ava.UI.ViewModels
return SortMode switch
{
#pragma warning disable IDE0055 // Disable formatting
+ ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName)
+ : SortExpressionComparer.Descending(app => app.TitleName),
+ ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer)
+ : SortExpressionComparer.Descending(app => app.Developer),
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
- ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes)
- : SortExpressionComparer.Descending(app => app.FileSizeBytes),
- ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum)
- : SortExpressionComparer.Descending(app => app.TimePlayedNum),
- ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName)
- : SortExpressionComparer.Descending(app => app.TitleName),
+ ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
+ ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension)
+ : SortExpressionComparer.Descending(app => app.FileExtension),
+ ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSize)
+ : SortExpressionComparer.Descending(app => app.FileSize),
+ ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path)
+ : SortExpressionComparer.Descending(app => app.Path),
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite)
: SortExpressionComparer.Descending(app => app.Favorite),
- ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer)
- : SortExpressionComparer.Descending(app => app.Developer),
- ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension)
- : SortExpressionComparer.Descending(app => app.FileExtension),
- ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path)
- : SortExpressionComparer.Descending(app => app.Path),
_ => null,
#pragma warning restore IDE0055
};
@@ -1488,7 +1489,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime();
- SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path);
+ SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
PrepareLoadScreen();
@@ -1547,13 +1548,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{
- if (appMetadata.LastPlayed.HasValue)
- {
- double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
- appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
- }
-
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePostGame();
});
}
@@ -1696,7 +1691,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
}
-
#endregion
}
}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
index dd0b92a51..5090a8c70 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
@@ -170,7 +170,9 @@ namespace Ryujinx.Ava.UI.ViewModels
try
{
- (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0);
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(file.AsStorage()).ThrowIfFailure();
+ (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null)
{
diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
index 2ab42e6ee..d636873a3 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml
@@ -5,6 +5,7 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
@@ -460,7 +461,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
index a98f08825..a6b587f67 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
@@ -23,11 +24,11 @@
Margin="0"
HorizontalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionGyroSensitivity}" />
-
-
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
index f633c0ed2..5b7087a47 100644
--- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml
@@ -1,6 +1,7 @@
-
-
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
index 32524740b..01133a4bc 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
@@ -176,6 +177,7 @@
Content="{Binding VolumeStatusText}"
IsChecked="{Binding VolumeMuted}"
IsVisible="{Binding !ShowLoadProgress}"
+ PointerWheelChanged="VolumeStatus_OnPointerWheelChanged"
Background="Transparent"
BorderThickness="0"
CornerRadius="0">
@@ -192,7 +194,7 @@
- 0,
+ > 1 => 1,
+ _ => newValue,
+ };
+
+ e.Handled = true;
+ }
}
}
diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
index 34624b222..cc21b5c60 100644
--- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
@@ -50,7 +51,7 @@
VerticalAlignment="Center"
Text="{locale:Locale IconSize}"
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
-
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml
index 5dc0fef5d..657e07ee7 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml
@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
@@ -63,13 +64,13 @@
Maximum="100" />
-
@@ -77,4 +78,4 @@
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
index d5039c131..629cdfc34 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
@@ -177,7 +178,7 @@
-
+ /// An event which works similarly to an AutoResetEvent, but is backed by a
+ /// more precise timer that allows waits of less than a millisecond.
+ ///
+ public interface IPreciseSleepEvent : IDisposable
+ {
+ ///
+ /// Adjust a timepoint to better fit the host clock.
+ /// When no adjustment is made, the input timepoint will be returned.
+ ///
+ /// Timepoint to adjust
+ /// Requested timeout in nanoseconds
+ /// Adjusted timepoint
+ long AdjustTimePoint(long timePoint, long timeoutNs);
+
+ ///
+ /// Sleep until a timepoint, or a signal is received.
+ /// Given no signal, may wake considerably before, or slightly after the timeout.
+ ///
+ /// Timepoint to sleep until
+ /// True if signalled or waited, false if a wait could not be performed
+ bool SleepUntil(long timePoint);
+
+ ///
+ /// Sleep until a signal is received.
+ ///
+ void Sleep();
+
+ ///
+ /// Signal the event, waking any sleeping thread or the next attempted sleep.
+ ///
+ void Signal();
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/Nanosleep.cs b/src/Ryujinx.Common/PreciseSleep/Nanosleep.cs
new file mode 100644
index 000000000..67f067ae2
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/Nanosleep.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ ///
+ /// Access to Linux/MacOS nanosleep, with platform specific bias to improve precision.
+ ///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("android")]
+ [SupportedOSPlatform("ios")]
+ internal static partial class Nanosleep
+ {
+ private const long LinuxBaseNanosleepBias = 50000; // 0.05ms
+
+ // Penalty for max allowed sleep duration
+ private const long LinuxNanosleepAccuracyPenaltyThreshold = 200000; // 0.2ms
+ private const long LinuxNanosleepAccuracyPenalty = 30000; // 0.03ms
+
+ // Penalty for base sleep duration
+ private const long LinuxNanosleepBasePenaltyThreshold = 500000; // 0.5ms
+ private const long LinuxNanosleepBasePenalty = 30000; // 0.03ms
+ private const long LinuxNanosleepPenaltyPerMillisecond = 18000; // 0.018ms
+ private const long LinuxNanosleepPenaltyCap = 18000; // 0.018ms
+
+ private const long LinuxStrictBiasOffset = 150_000; // 0.15ms
+
+ // Nanosleep duration is biased depending on the requested timeout on MacOS.
+ // These match the results when measuring on an M1 processor at AboveNormal priority.
+ private const long MacosBaseNanosleepBias = 5000; // 0.005ms
+ private const long MacosBiasPerMillisecond = 140000; // 0.14ms
+ private const long MacosBiasMaxNanoseconds = 20_000_000; // 20ms
+ private const long MacosStrictBiasOffset = 150_000; // 0.15ms
+
+ public static long Bias { get; }
+
+ ///
+ /// Get bias for a given nanosecond timeout.
+ /// Some platforms calculate their bias differently, this method can be used to counteract it.
+ ///
+ /// Nanosecond timeout
+ /// Bias in nanoseconds
+ public static long GetBias(long timeoutNs)
+ {
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
+ {
+ long biasNs = Math.Min(timeoutNs, MacosBiasMaxNanoseconds);
+ return MacosBaseNanosleepBias + biasNs * MacosBiasPerMillisecond / 1_000_000;
+ }
+ else
+ {
+ long bias = LinuxBaseNanosleepBias;
+
+ if (timeoutNs > LinuxNanosleepBasePenaltyThreshold)
+ {
+ long penalty = (timeoutNs - LinuxNanosleepBasePenaltyThreshold) * LinuxNanosleepPenaltyPerMillisecond / 1_000_000;
+ bias += LinuxNanosleepBasePenalty + Math.Min(LinuxNanosleepPenaltyCap, penalty);
+ }
+
+ return bias;
+ }
+ }
+
+ ///
+ /// Get a stricter bias for a given nanosecond timeout,
+ /// which can improve the chances the sleep completes before the timeout.
+ /// Some platforms calculate their bias differently, this method can be used to counteract it.
+ ///
+ /// Nanosecond timeout
+ /// Strict bias in nanoseconds
+ public static long GetStrictBias(long timeoutNs)
+ {
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
+ {
+ return GetBias(timeoutNs) + MacosStrictBiasOffset;
+ }
+ else
+ {
+ long bias = GetBias(timeoutNs) + LinuxStrictBiasOffset;
+
+ if (timeoutNs > LinuxNanosleepAccuracyPenaltyThreshold)
+ {
+ bias += LinuxNanosleepAccuracyPenalty;
+ }
+
+ return bias;
+ }
+ }
+
+ static Nanosleep()
+ {
+ Bias = GetBias(0);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct Timespec
+ {
+ public long tv_sec; // Seconds
+ public long tv_nsec; // Nanoseconds
+ }
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int nanosleep(ref Timespec req, ref Timespec rem);
+
+ ///
+ /// Convert a timeout in nanoseconds to a timespec for nanosleep.
+ ///
+ /// Timeout in nanoseconds
+ /// Timespec for nanosleep
+ private static Timespec GetTimespecFromNanoseconds(ulong nanoseconds)
+ {
+ return new Timespec
+ {
+ tv_sec = (long)(nanoseconds / 1_000_000_000),
+ tv_nsec = (long)(nanoseconds % 1_000_000_000)
+ };
+ }
+
+ ///
+ /// Sleep for approximately a given time period in nanoseconds.
+ ///
+ /// Time to sleep for in nanoseconds
+ public static void Sleep(long nanoseconds)
+ {
+ nanoseconds -= GetBias(nanoseconds);
+
+ if (nanoseconds >= 0)
+ {
+ Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
+ Timespec rem = new();
+
+ nanosleep(ref req, ref rem);
+ }
+ }
+
+ ///
+ /// Sleep for at most a given time period in nanoseconds.
+ /// Uses a stricter bias to wake before the requested duration.
+ ///
+ ///
+ /// Due to OS scheduling behaviour, this timeframe may still be missed.
+ ///
+ /// Maximum allowed time for sleep
+ public static void SleepAtMost(long nanoseconds)
+ {
+ // Stricter bias to ensure we wake before the timepoint.
+ nanoseconds -= GetStrictBias(nanoseconds);
+
+ if (nanoseconds >= 0)
+ {
+ Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
+ Timespec rem = new();
+
+ nanosleep(ref req, ref rem);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/NanosleepEvent.cs b/src/Ryujinx.Common/PreciseSleep/NanosleepEvent.cs
new file mode 100644
index 000000000..f54fb09c1
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/NanosleepEvent.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ ///
+ /// A precise sleep event for linux and macos that uses nanosleep for more precise timeouts.
+ ///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("android")]
+ [SupportedOSPlatform("ios")]
+ internal class NanosleepEvent : IPreciseSleepEvent
+ {
+ private readonly AutoResetEvent _waitEvent = new(false);
+ private readonly NanosleepPool _pool;
+
+ public NanosleepEvent()
+ {
+ _pool = new NanosleepPool(_waitEvent);
+ }
+
+ public long AdjustTimePoint(long timePoint, long timeoutNs)
+ {
+ // No adjustment
+ return timePoint;
+ }
+
+ public bool SleepUntil(long timePoint)
+ {
+ long now = PerformanceCounter.ElapsedTicks;
+ long delta = (timePoint - now);
+ long ms = Math.Min(delta / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
+ long ns = (delta * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
+
+ if (ms > 0)
+ {
+ _waitEvent.WaitOne((int)ms);
+
+ return true;
+ }
+ else if (ns - Nanosleep.Bias > 0)
+ {
+ // Don't bother starting a sleep if there's already a signal active.
+ if (_waitEvent.WaitOne(0))
+ {
+ return true;
+ }
+
+ // The 1ms wait will be interrupted by the nanosleep timeout if it completes.
+ if (!_pool.SleepAndSignal(ns, timePoint))
+ {
+ // Too many threads on the pool.
+ return false;
+ }
+ _waitEvent.WaitOne(1);
+ _pool.IgnoreSignal();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Sleep()
+ {
+ _waitEvent.WaitOne();
+ }
+
+ public void Signal()
+ {
+ _waitEvent.Set();
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ _pool.Dispose();
+ _waitEvent.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs b/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
new file mode 100644
index 000000000..c0973dcb3
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ ///
+ /// A pool of threads used to allow "interruptable" nanosleep for a single target event.
+ ///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("android")]
+ [SupportedOSPlatform("ios")]
+ internal class NanosleepPool : IDisposable
+ {
+ public const int MaxThreads = 8;
+
+ ///
+ /// A thread that nanosleeps and may signal an event on wake.
+ /// When a thread is assigned a nanosleep to perform, it also gets a signal ID.
+ /// The pool's target event is only signalled if this ID matches the latest dispatched one.
+ ///
+ private class NanosleepThread : IDisposable
+ {
+ private static readonly long _timePointEpsilon;
+
+ static NanosleepThread()
+ {
+ _timePointEpsilon = PerformanceCounter.TicksPerMillisecond / 100; // 0.01ms
+ }
+
+ private readonly Thread _thread;
+ private readonly NanosleepPool _parent;
+ private readonly AutoResetEvent _newWaitEvent;
+ private bool _running = true;
+
+ private long _signalId;
+ private long _nanoseconds;
+ private long _timePoint;
+
+ public long SignalId => _signalId;
+
+ ///
+ /// Creates a new NanosleepThread for a parent pool, with a specified thread ID.
+ ///
+ /// Parent NanosleepPool
+ /// Thread ID
+ public NanosleepThread(NanosleepPool parent, int id)
+ {
+ _parent = parent;
+ _newWaitEvent = new(false);
+
+ _thread = new Thread(Loop)
+ {
+ Name = $"Common.Nanosleep.{id}",
+ Priority = ThreadPriority.AboveNormal,
+ IsBackground = true
+ };
+
+ _thread.Start();
+ }
+
+ ///
+ /// Service requests to perform a nanosleep, signal parent pool when complete.
+ ///
+ private void Loop()
+ {
+ _newWaitEvent.WaitOne();
+
+ while (_running)
+ {
+ Nanosleep.Sleep(_nanoseconds);
+
+ _parent.Signal(this);
+ _newWaitEvent.WaitOne();
+ }
+
+ _newWaitEvent.Dispose();
+ }
+
+ ///
+ /// Assign a nanosleep for this thread to perform, then signal at the end.
+ ///
+ /// Nanoseconds to sleep
+ /// Signal ID
+ /// Target timepoint
+ public void SleepAndSignal(long nanoseconds, long signalId, long timePoint)
+ {
+ _signalId = signalId;
+ _nanoseconds = nanoseconds;
+ _timePoint = timePoint;
+ _newWaitEvent.Set();
+ }
+
+ ///
+ /// Resurrect an active nanosleep's signal if its target timepoint is a close enough match.
+ ///
+ /// New signal id to assign the nanosleep
+ /// Target timepoint
+ /// True if resurrected, false otherwise
+ public bool Resurrect(long signalId, long timePoint)
+ {
+ if (Math.Abs(timePoint - _timePoint) < _timePointEpsilon)
+ {
+ _signalId = signalId;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Dispose the NanosleepThread, interrupting its worker loop.
+ ///
+ public void Dispose()
+ {
+ if (_running)
+ {
+ _running = false;
+ _newWaitEvent.Set();
+ }
+ }
+ }
+
+ private readonly object _lock = new();
+ private readonly List _threads = new();
+ private readonly List _active = new();
+ private readonly Stack _free = new();
+ private readonly AutoResetEvent _signalTarget;
+
+ private long _signalId;
+
+ ///
+ /// Creates a new NanosleepPool with a target event to signal when a nanosleep completes.
+ ///
+ /// Event to signal when nanosleeps complete
+ public NanosleepPool(AutoResetEvent signalTarget)
+ {
+ _signalTarget = signalTarget;
+ }
+
+ ///
+ /// Signal the target event (if the source sleep has not been superseded)
+ /// and free the nanosleep thread.
+ ///
+ /// Nanosleep thread that completed
+ private void Signal(NanosleepThread thread)
+ {
+ lock (_lock)
+ {
+ _active.Remove(thread);
+ _free.Push(thread);
+
+ if (thread.SignalId == _signalId)
+ {
+ _signalTarget.Set();
+ }
+ }
+ }
+
+ ///
+ /// Sleep for the given number of nanoseconds and signal the target event.
+ /// This does not block the caller thread.
+ ///
+ /// Nanoseconds to sleep
+ /// Target timepoint
+ /// True if the signal will be set, false otherwise
+ public bool SleepAndSignal(long nanoseconds, long timePoint)
+ {
+ lock (_lock)
+ {
+ _signalId++;
+
+ // Check active sleeps, if any line up with the requested timepoint then resurrect that nanosleep.
+ foreach (NanosleepThread existing in _active)
+ {
+ if (existing.Resurrect(_signalId, timePoint))
+ {
+ return true;
+ }
+ }
+
+ if (!_free.TryPop(out NanosleepThread thread))
+ {
+ if (_threads.Count >= MaxThreads)
+ {
+ return false;
+ }
+
+ thread = new NanosleepThread(this, _threads.Count);
+
+ _threads.Add(thread);
+ }
+
+ _active.Add(thread);
+
+ thread.SleepAndSignal(nanoseconds, _signalId, timePoint);
+
+ return true;
+ }
+ }
+
+ ///
+ /// Ignore the latest nanosleep.
+ ///
+ public void IgnoreSignal()
+ {
+ _signalId++;
+ }
+
+ ///
+ /// Dispose the NanosleepPool, disposing all of its active threads.
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ foreach (NanosleepThread thread in _threads)
+ {
+ thread.Dispose();
+ }
+
+ _threads.Clear();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/PreciseSleepHelper.cs b/src/Ryujinx.Common/PreciseSleep/PreciseSleepHelper.cs
new file mode 100644
index 000000000..3c30a7f60
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/PreciseSleepHelper.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.SystemInterop;
+using System;
+using System.Threading;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ public static class PreciseSleepHelper
+ {
+ ///
+ /// Create a precise sleep event for the current platform.
+ ///
+ /// A precise sleep event
+ public static IPreciseSleepEvent CreateEvent()
+ {
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
+ {
+ return new NanosleepEvent();
+ }
+ else if (OperatingSystem.IsWindows())
+ {
+ return new WindowsSleepEvent();
+ }
+ else
+ {
+ return new SleepEvent();
+ }
+ }
+
+ ///
+ /// Sleeps up to the closest point to the timepoint that the OS reasonably allows.
+ /// The provided event is used by the timer to wake the current thread, and should not be signalled from any other source.
+ ///
+ /// Event used to wake this thread
+ /// Target timepoint in host ticks
+ public static void SleepUntilTimePoint(EventWaitHandle evt, long timePoint)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ WindowsGranularTimer.Instance.SleepUntilTimePointWithoutExternalSignal(evt, timePoint);
+ }
+ else
+ {
+ // Events might oversleep by a little, depending on OS.
+ // We don't want to miss the timepoint, so bias the wait to be lower.
+ // Nanosleep can possibly handle it better, too.
+ long accuracyBias = PerformanceCounter.TicksPerMillisecond / 2;
+ long now = PerformanceCounter.ElapsedTicks + accuracyBias;
+ long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
+
+ if (ms > 0)
+ {
+ evt.WaitOne((int)ms);
+ }
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
+ {
+ // Do a nanosleep.
+ now = PerformanceCounter.ElapsedTicks;
+ long ns = ((timePoint - now) * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
+
+ Nanosleep.SleepAtMost(ns);
+ }
+ }
+ }
+
+ ///
+ /// Spinwait until the given timepoint. If wakeSignal is or becomes 1, return early.
+ /// Thread is allowed to yield.
+ ///
+ /// Target timepoint in host ticks
+ /// Returns early if this is set to 1
+ public static void SpinWaitUntilTimePoint(long timePoint, ref long wakeSignal)
+ {
+ SpinWait spinWait = new();
+
+ while (Interlocked.Read(ref wakeSignal) != 1 && PerformanceCounter.ElapsedTicks < timePoint)
+ {
+ // Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
+ if (spinWait.NextSpinWillYield)
+ {
+ Thread.Yield();
+
+ spinWait.Reset();
+ }
+ else
+ {
+ spinWait.SpinOnce();
+ }
+ }
+ }
+
+ ///
+ /// Spinwait until the given timepoint, with no opportunity to wake early.
+ ///
+ /// Target timepoint in host ticks
+ public static void SpinWaitUntilTimePoint(long timePoint)
+ {
+ while (PerformanceCounter.ElapsedTicks < timePoint)
+ {
+ Thread.SpinWait(5);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/SleepEvent.cs b/src/Ryujinx.Common/PreciseSleep/SleepEvent.cs
new file mode 100644
index 000000000..f0769d1e4
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/SleepEvent.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ ///
+ /// A cross-platform precise sleep event that has millisecond granularity.
+ ///
+ internal class SleepEvent : IPreciseSleepEvent
+ {
+ private readonly AutoResetEvent _waitEvent = new(false);
+
+ public long AdjustTimePoint(long timePoint, long timeoutNs)
+ {
+ // No adjustment
+ return timePoint;
+ }
+
+ public bool SleepUntil(long timePoint)
+ {
+ long now = PerformanceCounter.ElapsedTicks;
+ long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
+
+ if (ms > 0)
+ {
+ _waitEvent.WaitOne((int)ms);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Sleep()
+ {
+ _waitEvent.WaitOne();
+ }
+
+ public void Signal()
+ {
+ _waitEvent.Set();
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ _waitEvent.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
new file mode 100644
index 000000000..a0de16341
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ ///
+ /// Timer that attempts to align with the hardware timer interrupt,
+ /// and can alert listeners on ticks.
+ ///
+ [SupportedOSPlatform("windows")]
+ internal partial class WindowsGranularTimer
+ {
+ private const int MinimumGranularity = 5000;
+
+ private static readonly WindowsGranularTimer _instance = new();
+ public static WindowsGranularTimer Instance => _instance;
+
+ private readonly struct WaitingObject
+ {
+ public readonly long Id;
+ public readonly EventWaitHandle Signal;
+ public readonly long TimePoint;
+
+ public WaitingObject(long id, EventWaitHandle signal, long timePoint)
+ {
+ Id = id;
+ Signal = signal;
+ TimePoint = timePoint;
+ }
+ }
+
+ [LibraryImport("ntdll.dll", SetLastError = true)]
+ private static partial int NtSetTimerResolution(int DesiredResolution, [MarshalAs(UnmanagedType.Bool)] bool SetResolution, out int CurrentResolution);
+
+ [LibraryImport("ntdll.dll", SetLastError = true)]
+ private static partial int NtQueryTimerResolution(out int MaximumResolution, out int MinimumResolution, out int CurrentResolution);
+
+ [LibraryImport("ntdll.dll", SetLastError = true)]
+ private static partial uint NtDelayExecution([MarshalAs(UnmanagedType.Bool)] bool Alertable, ref long DelayInterval);
+
+ public long GranularityNs => _granularityNs;
+ public long GranularityTicks => _granularityTicks;
+
+ private readonly Thread _timerThread;
+ private long _granularityNs = MinimumGranularity * 100L;
+ private long _granularityTicks;
+ private long _lastTicks = PerformanceCounter.ElapsedTicks;
+ private long _lastId;
+
+ private readonly object _lock = new();
+ private readonly List _waitingObjects = new();
+
+ private WindowsGranularTimer()
+ {
+ _timerThread = new Thread(Loop)
+ {
+ IsBackground = true,
+ Name = "Common.WindowsTimer",
+ Priority = ThreadPriority.Highest
+ };
+
+ _timerThread.Start();
+ }
+
+ ///
+ /// Measure and initialize the timer's target granularity.
+ ///
+ private void Initialize()
+ {
+ NtQueryTimerResolution(out _, out int min, out int curr);
+
+ if (min > 0)
+ {
+ min = Math.Max(min, MinimumGranularity);
+
+ _granularityNs = min * 100L;
+ NtSetTimerResolution(min, true, out _);
+ }
+ else
+ {
+ _granularityNs = curr * 100L;
+ }
+
+ _granularityTicks = (_granularityNs * PerformanceCounter.TicksPerMillisecond) / 1_000_000;
+ }
+
+ ///
+ /// Main loop for the timer thread. Wakes every clock tick and signals any listeners,
+ /// as well as keeping track of clock alignment.
+ ///
+ private void Loop()
+ {
+ Initialize();
+ while (true)
+ {
+ long delayInterval = -1; // Next tick
+ NtSetTimerResolution((int)(_granularityNs / 100), true, out _);
+ NtDelayExecution(false, ref delayInterval);
+
+ long newTicks = PerformanceCounter.ElapsedTicks;
+ long nextTicks = newTicks + _granularityTicks;
+
+ lock (_lock)
+ {
+ for (int i = 0; i < _waitingObjects.Count; i++)
+ {
+ if (nextTicks > _waitingObjects[i].TimePoint)
+ {
+ // The next clock tick will be after the timepoint, we need to signal now.
+ _waitingObjects[i].Signal.Set();
+
+ _waitingObjects.RemoveAt(i--);
+ }
+ }
+
+ _lastTicks = newTicks;
+ }
+ }
+ }
+
+ ///
+ /// Sleep until a timepoint.
+ ///
+ /// Reset event to use to be awoken by the clock tick, or an external signal
+ /// Target timepoint
+ /// True if waited or signalled, false otherwise
+ public bool SleepUntilTimePoint(AutoResetEvent evt, long timePoint)
+ {
+ if (evt.WaitOne(0))
+ {
+ return true;
+ }
+
+ long id;
+
+ lock (_lock)
+ {
+ // Return immediately if the next tick is after the requested timepoint.
+ long nextTicks = _lastTicks + _granularityTicks;
+
+ if (nextTicks > timePoint)
+ {
+ return false;
+ }
+
+ id = ++_lastId;
+
+ _waitingObjects.Add(new WaitingObject(id, evt, timePoint));
+ }
+
+ evt.WaitOne();
+
+ lock (_lock)
+ {
+ for (int i = 0; i < _waitingObjects.Count; i++)
+ {
+ if (id == _waitingObjects[i].Id)
+ {
+ _waitingObjects.RemoveAt(i--);
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Sleep until a timepoint, but don't expect any external signals.
+ ///
+ ///
+ /// Saves some effort compared to the sleep that expects to be signalled.
+ ///
+ /// Reset event to use to be awoken by the clock tick
+ /// Target timepoint
+ /// True if waited, false otherwise
+ public bool SleepUntilTimePointWithoutExternalSignal(EventWaitHandle evt, long timePoint)
+ {
+ long id;
+
+ lock (_lock)
+ {
+ // Return immediately if the next tick is after the requested timepoint.
+ long nextTicks = _lastTicks + _granularityTicks;
+
+ if (nextTicks > timePoint)
+ {
+ return false;
+ }
+
+ id = ++_lastId;
+
+ _waitingObjects.Add(new WaitingObject(id, evt, timePoint));
+ }
+
+ evt.WaitOne();
+
+ return true;
+ }
+
+ ///
+ /// Returns the two nearest clock ticks for a given timepoint.
+ ///
+ /// Target timepoint
+ /// The nearest clock ticks before and after the given timepoint
+ public (long, long) ReturnNearestTicks(long timePoint)
+ {
+ long last = _lastTicks;
+ long delta = timePoint - last;
+
+ long lowTicks = delta / _granularityTicks;
+ long highTicks = (delta + _granularityTicks - 1) / _granularityTicks;
+
+ return (last + lowTicks * _granularityTicks, last + highTicks * _granularityTicks);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/PreciseSleep/WindowsSleepEvent.cs b/src/Ryujinx.Common/PreciseSleep/WindowsSleepEvent.cs
new file mode 100644
index 000000000..87c10d18e
--- /dev/null
+++ b/src/Ryujinx.Common/PreciseSleep/WindowsSleepEvent.cs
@@ -0,0 +1,92 @@
+using Ryujinx.Common.SystemInterop;
+using System;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Common.PreciseSleep
+{
+ ///
+ /// A precise sleep event that uses Windows specific methods to increase clock resolution beyond 1ms,
+ /// use the clock's phase for more precise waits, and potentially align timepoints with it.
+ ///
+ [SupportedOSPlatform("windows")]
+ internal class WindowsSleepEvent : IPreciseSleepEvent
+ {
+ ///
+ /// The clock can drift a bit, so add this to encourage the clock to still wait if the next tick is forecasted slightly before it.
+ ///
+ private const long ErrorBias = 50000;
+
+ ///
+ /// Allowed to be 0.05ms away from the clock granularity to reduce precision.
+ ///
+ private const long ClockAlignedBias = 50000;
+
+ ///
+ /// The fraction of clock granularity above the timepoint that will align it down to the lower timepoint.
+ /// Currently set to the lower 1/4, so for 0.5ms granularity: 0.1ms would be rounded down, 0.2 ms would be rounded up.
+ ///
+ private const long ReverseTimePointFraction = 4;
+
+ private readonly AutoResetEvent _waitEvent = new(false);
+ private readonly WindowsGranularTimer _timer = WindowsGranularTimer.Instance;
+
+ ///
+ /// Set to true to disable timepoint realignment.
+ ///
+ public bool Precise { get; set; } = false;
+
+ public long AdjustTimePoint(long timePoint, long timeoutNs)
+ {
+ if (Precise || timePoint == long.MaxValue)
+ {
+ return timePoint;
+ }
+
+ // Does the timeout align with the host clock?
+
+ long granularity = _timer.GranularityNs;
+ long misalignment = timeoutNs % granularity;
+
+ if ((misalignment < ClockAlignedBias || misalignment > granularity - ClockAlignedBias) && timeoutNs > ClockAlignedBias)
+ {
+ // Inaccurate sleep for 0.5ms increments, typically.
+
+ (long low, long high) = _timer.ReturnNearestTicks(timePoint);
+
+ if (timePoint - low < _timer.GranularityTicks / ReverseTimePointFraction)
+ {
+ timePoint = low;
+ }
+ else
+ {
+ timePoint = high;
+ }
+ }
+
+ return timePoint;
+ }
+
+ public bool SleepUntil(long timePoint)
+ {
+ return _timer.SleepUntilTimePoint(_waitEvent, timePoint + (ErrorBias * PerformanceCounter.TicksPerMillisecond) / 1_000_000);
+ }
+
+ public void Sleep()
+ {
+ _waitEvent.WaitOne();
+ }
+
+ public void Signal()
+ {
+ _waitEvent.Set();
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ _waitEvent.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj
index c02b11e0c..da2f13a21 100644
--- a/src/Ryujinx.Common/Ryujinx.Common.csproj
+++ b/src/Ryujinx.Common/Ryujinx.Common.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
$(DefineConstants);$(ExtraDefineConstants)
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
index 78fb342b1..3b64a28f5 100644
--- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities
{
return ConvertIpv4Address(IPAddress.Parse(ipAddress));
}
+
+ public static IPAddress ConvertUint(uint ipAddress)
+ {
+ return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) });
+ }
}
}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
new file mode 100644
index 000000000..876597b78
--- /dev/null
+++ b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+
+namespace Ryujinx.Cpu.AppleHv
+{
+ static class HvCodePatcher
+ {
+ private const uint XMask = 0x3f808000u;
+ private const uint XValue = 0x8000000u;
+
+ private const uint ZrIndex = 31u;
+
+ public static void RewriteUnorderedExclusiveInstructions(Span code)
+ {
+ Span codeUint = MemoryMarshal.Cast(code);
+ Span> codeVector = MemoryMarshal.Cast>(code);
+
+ Vector128 mask = Vector128.Create(XMask);
+ Vector128 value = Vector128.Create(XValue);
+
+ for (int index = 0; index < codeVector.Length; index++)
+ {
+ Vector128 v = codeVector[index];
+
+ if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value))
+ {
+ int baseIndex = index * 4;
+
+ for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++)
+ {
+ ref uint inst = ref codeUint[instIndex];
+
+ if ((inst & XMask) != XValue)
+ {
+ continue;
+ }
+
+ bool isPair = (inst & (1u << 21)) != 0;
+ bool isLoad = (inst & (1u << 22)) != 0;
+
+ uint rt2 = (inst >> 10) & 0x1fu;
+ uint rs = (inst >> 16) & 0x1fu;
+
+ if (isLoad && rs != ZrIndex)
+ {
+ continue;
+ }
+
+ if (!isPair && rt2 != ZrIndex)
+ {
+ continue;
+ }
+
+ // Set the ordered flag.
+ inst |= 1u << 15;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
index d5ce817a4..947c37100 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
@@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv
}
}
-#pragma warning disable IDE0051 // Remove unused private member
- ///
- /// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
- ///
- /// Virtual address of the range
- /// Size of the range in bytes
- private void AssertMapped(ulong va, ulong size)
- {
- if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
- {
- throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
- }
- }
-#pragma warning restore IDE0051
-
///
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
@@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv
return (int)(vaSpan / PageSize);
}
+ ///
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ if (protection.HasFlag(MemoryPermission.Execute))
+ {
+ // Some applications use unordered exclusive memory access instructions
+ // where it is not valid to do so, leading to memory re-ordering that
+ // makes the code behave incorrectly on some CPUs.
+ // To work around this, we force all such accesses to be ordered.
+
+ using WritableRegion writableRegion = GetWritableRegion(va, (int)size);
+
+ HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span);
+ }
+
+ // TODO
+ }
+
///
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
index 1c27e97fa..912e3f7e3 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
@@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit
}
}
-#pragma warning disable IDE0051 // Remove unused private member
- private ulong GetPhysicalAddress(ulong va)
- {
- // We return -1L if the virtual address is invalid or unmapped.
- if (!ValidateAddress(va) || !IsMapped(va))
- {
- return ulong.MaxValue;
- }
-
- return GetPhysicalAddressInternal(va);
- }
-#pragma warning restore IDE0051
-
private ulong GetPhysicalAddressInternal(ulong va)
{
return PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
}
+ ///
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ // TODO
+ }
+
///
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{
@@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit
/// Disposes of resources used by the memory manager.
///
protected override void Destroy() => _pageTable.Dispose();
-
-#pragma warning disable IDE0051 // Remove unused private member
- private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
-#pragma warning restore IDE0051
}
}
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
index 010a0bc25..6d32787ac 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs
@@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit
return (int)(vaSpan / PageSize);
}
+ ///
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ // TODO
+ }
+
///
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{
diff --git a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
index 7da8da25a..5a6bf5c3d 100644
--- a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
+++ b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
index 082dac9c2..ae2821edb 100644
--- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
+++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index 769967d63..961848d72 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64;
+ public readonly bool SupportsTextureGatherOffsets;
public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsVertexStoreAndAtomics;
public readonly bool SupportsViewportIndexVertexTessellation;
@@ -93,6 +94,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsShaderBallot,
bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64,
+ bool supportsTextureGatherOffsets,
bool supportsTextureShadowLod,
bool supportsVertexStoreAndAtomics,
bool supportsViewportIndexVertexTessellation,
@@ -144,6 +146,7 @@ namespace Ryujinx.Graphics.GAL
SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64;
+ SupportsTextureGatherOffsets = supportsTextureGatherOffsets;
SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;
SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation;
diff --git a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
index 189108a39..d88b641a3 100644
--- a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
+++ b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
index e6557780b..b1921bd51 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs
@@ -211,6 +211,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
int xCount = (int)_state.State.LineLengthIn;
int yCount = (int)_state.State.LineCount;
+ _channel.TextureManager.RefreshModifiedTextures();
_3dEngine.CreatePendingSyncs();
_3dEngine.FlushUboDirty();
@@ -279,7 +280,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
bool completeSource = IsTextureCopyComplete(src, srcLinear, srcBpp, srcStride, xCount, yCount);
bool completeDest = IsTextureCopyComplete(dst, dstLinear, dstBpp, dstStride, xCount, yCount);
- if (completeSource && completeDest)
+ // Try to set the texture data directly,
+ // but only if we are doing a complete copy,
+ // and not for block linear to linear copies, since those are typically accessed from the CPU.
+
+ if (completeSource && completeDest && !(dstLinear && !srcLinear))
{
var target = memoryManager.Physical.TextureCache.FindTexture(
memoryManager,
diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
index 1b517e63f..0af0725a2 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs
@@ -651,9 +651,35 @@ namespace Ryujinx.Graphics.Gpu.Image
/// True if the format is valid, false otherwise
public static bool TryGetTextureFormat(uint encoded, bool isSrgb, out FormatInfo format)
{
- encoded |= (isSrgb ? 1u << 19 : 0u);
+ bool isPacked = (encoded & 0x80000000u) != 0;
+ if (isPacked)
+ {
+ encoded &= ~0x80000000u;
+ }
- return _textureFormats.TryGetValue((TextureFormat)encoded, out format);
+ encoded |= isSrgb ? 1u << 19 : 0u;
+
+ bool found = _textureFormats.TryGetValue((TextureFormat)encoded, out format);
+
+ if (found && isPacked && !format.Format.IsDepthOrStencil())
+ {
+ // If the packed flag is set, then the components of the pixel are tightly packed into the
+ // GPU registers on the shader.
+ // We can get the same behaviour by aliasing the texture as a format with the same amount of
+ // bytes per pixel, but only a single or the lowest possible number of components.
+
+ format = format.BytesPerPixel switch
+ {
+ 1 => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1),
+ 2 => new FormatInfo(Format.R16Unorm, 1, 1, 2, 1),
+ 4 => new FormatInfo(Format.R32Float, 1, 1, 4, 1),
+ 8 => new FormatInfo(Format.R32G32Float, 1, 1, 8, 2),
+ 16 => new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4),
+ _ => format,
+ };
+ }
+
+ return found;
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 022a3839f..f1615b388 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -149,6 +149,7 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public bool HadPoolOwner { get; private set; }
+ ///
/// Physical memory ranges where the texture data is located.
///
public MultiRange Range { get; private set; }
@@ -1700,7 +1701,6 @@ namespace Ryujinx.Graphics.Gpu.Image
if (Group.Storage == this)
{
Group.Unmapped();
-
Group.ClearModified(unmapRange);
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 5048ccca4..6b92c0aaf 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -107,8 +107,6 @@ namespace Ryujinx.Graphics.Gpu.Image
// Any texture that has been unmapped at any point or is partially unmapped
// should update their pool references after the remap completes.
- MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
-
foreach (var texture in _partiallyMappedTextures)
{
texture.UpdatePoolMappings();
@@ -735,9 +733,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (overlap.IsView)
{
- overlapCompatibility = overlapCompatibility == TextureViewCompatibility.FormatAlias ?
- TextureViewCompatibility.Incompatible :
- TextureViewCompatibility.CopyOnly;
+ overlapCompatibility = TextureViewCompatibility.CopyOnly;
}
else
{
@@ -815,7 +811,7 @@ namespace Ryujinx.Graphics.Gpu.Image
Texture overlap = _textureOverlaps[index];
OverlapInfo oInfo = _overlapInfo[index];
- if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible || oInfo.Compatibility == TextureViewCompatibility.FormatAlias)
+ if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
{
if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility))
{
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
index 3a0efcdda..eef38948d 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs
@@ -2,6 +2,8 @@ using Ryujinx.Common;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Texture;
using System;
+using System.Diagnostics;
+using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Image
{
@@ -226,7 +228,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
// D32F and R32F texture have the same representation internally,
// however the R32F format is used to sample from depth textures.
- if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || depthAlias))
+ if (IsValidDepthAsColorAlias(lhs.FormatInfo.Format, rhs.FormatInfo.Format) && (forSampler || depthAlias))
{
return TextureMatchQuality.FormatAlias;
}
@@ -239,14 +241,8 @@ namespace Ryujinx.Graphics.Gpu.Image
{
return TextureMatchQuality.FormatAlias;
}
-
- if (lhs.FormatInfo.Format == Format.D16Unorm && rhs.FormatInfo.Format == Format.R16Unorm)
- {
- return TextureMatchQuality.FormatAlias;
- }
-
- if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
- lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
+ else if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
+ lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
{
return TextureMatchQuality.FormatAlias;
}
@@ -345,7 +341,20 @@ namespace Ryujinx.Graphics.Gpu.Image
if (lhs.FormatInfo.BytesPerPixel != rhs.FormatInfo.BytesPerPixel && IsIncompatibleFormatAliasingAllowed(lhs.FormatInfo, rhs.FormatInfo))
{
- alignedWidthMatches = lhsSize.Width * lhs.FormatInfo.BytesPerPixel == rhsSize.Width * rhs.FormatInfo.BytesPerPixel;
+ // If the formats are incompatible, but the texture strides match,
+ // we might allow them to be copy compatible depending on the format.
+ // The strides are aligned because the format with higher bytes per pixel
+ // might need a bit of padding at the end due to one width not being a multiple of the other.
+
+ Debug.Assert((1 << BitOperations.Log2((uint)lhs.FormatInfo.BytesPerPixel)) == lhs.FormatInfo.BytesPerPixel);
+ Debug.Assert((1 << BitOperations.Log2((uint)rhs.FormatInfo.BytesPerPixel)) == rhs.FormatInfo.BytesPerPixel);
+
+ int alignment = Math.Max(lhs.FormatInfo.BytesPerPixel, rhs.FormatInfo.BytesPerPixel);
+
+ int lhsStride = BitUtils.AlignUp(lhsSize.Width * lhs.FormatInfo.BytesPerPixel, alignment);
+ int rhsStride = BitUtils.AlignUp(rhsSize.Width * rhs.FormatInfo.BytesPerPixel, alignment);
+
+ alignedWidthMatches = lhsStride == rhsStride;
}
TextureViewCompatibility result = TextureViewCompatibility.Full;
@@ -632,12 +641,27 @@ namespace Ryujinx.Graphics.Gpu.Image
if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil())
{
- return FormatMatches(lhs, rhs, flags.HasFlag(TextureSearchFlags.ForSampler), flags.HasFlag(TextureSearchFlags.DepthAlias)) switch
+ bool forSampler = flags.HasFlag(TextureSearchFlags.ForSampler);
+ bool depthAlias = flags.HasFlag(TextureSearchFlags.DepthAlias);
+
+ TextureMatchQuality matchQuality = FormatMatches(lhs, rhs, forSampler, depthAlias);
+
+ if (matchQuality == TextureMatchQuality.Perfect)
{
- TextureMatchQuality.Perfect => TextureViewCompatibility.Full,
- TextureMatchQuality.FormatAlias => TextureViewCompatibility.FormatAlias,
- _ => TextureViewCompatibility.Incompatible,
- };
+ return TextureViewCompatibility.Full;
+ }
+ else if (matchQuality == TextureMatchQuality.FormatAlias)
+ {
+ return TextureViewCompatibility.FormatAlias;
+ }
+ else if (IsValidColorAsDepthAlias(lhsFormat.Format, rhsFormat.Format) || IsValidDepthAsColorAlias(lhsFormat.Format, rhsFormat.Format))
+ {
+ return TextureViewCompatibility.CopyOnly;
+ }
+ else
+ {
+ return TextureViewCompatibility.Incompatible;
+ }
}
if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps))
@@ -666,6 +690,30 @@ namespace Ryujinx.Graphics.Gpu.Image
return TextureViewCompatibility.Incompatible;
}
+ ///
+ /// Checks if it's valid to alias a color format as a depth format.
+ ///
+ /// Source format to be checked
+ /// Target format to be checked
+ /// True if it's valid to alias the formats
+ private static bool IsValidColorAsDepthAlias(Format lhsFormat, Format rhsFormat)
+ {
+ return (lhsFormat == Format.R32Float && rhsFormat == Format.D32Float) ||
+ (lhsFormat == Format.R16Unorm && rhsFormat == Format.D16Unorm);
+ }
+
+ ///
+ /// Checks if it's valid to alias a depth format as a color format.
+ ///
+ /// Source format to be checked
+ /// Target format to be checked
+ /// True if it's valid to alias the formats
+ private static bool IsValidDepthAsColorAlias(Format lhsFormat, Format rhsFormat)
+ {
+ return (lhsFormat == Format.D32Float && rhsFormat == Format.R32Float) ||
+ (lhsFormat == Format.D16Unorm && rhsFormat == Format.R16Unorm);
+ }
+
///
/// Checks if aliasing of two formats that would normally be considered incompatible be allowed,
/// using copy dependencies.
@@ -685,7 +733,8 @@ namespace Ryujinx.Graphics.Gpu.Image
(lhsFormat, rhsFormat) = (rhsFormat, lhsFormat);
}
- return lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm;
+ return (lhsFormat.Format == Format.R8G8B8A8Unorm && rhsFormat.Format == Format.R32G32B32A32Float) ||
+ (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm);
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index 746a95ffc..b93f3832d 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -992,26 +992,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
- ///
- /// The action to perform when a memory tracking handle is flipped to dirty.
- /// This notifies overlapping textures that the memory needs to be synchronized.
- ///
- /// The handle that a dirty flag was set on
- private void DirtyAction(TextureGroupHandle groupHandle)
- {
- // Notify all textures that belong to this handle.
-
- Storage.SignalGroupDirty();
-
- lock (groupHandle.Overlaps)
- {
- foreach (Texture overlap in groupHandle.Overlaps)
- {
- overlap.SignalGroupDirty();
- }
- }
- }
-
///
/// Generate a CpuRegionHandle for a given address and size range in CPU VA.
///
@@ -1083,11 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Image
views,
result.ToArray());
- foreach (RegionHandle handle in result)
- {
- handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
- }
-
return groupHandle;
}
@@ -1359,11 +1334,6 @@ namespace Ryujinx.Graphics.Gpu.Image
var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles);
- foreach (RegionHandle handle in cpuRegionHandles)
- {
- handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
- }
-
handles = new TextureGroupHandle[] { groupHandle };
}
else
@@ -1619,6 +1589,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified)
{
handle.Modified = false;
+ handle.DeferredCopy = null;
Storage.SignalModifiedDirty();
lock (handle.Overlaps)
@@ -1659,6 +1630,12 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
+ // If size is zero, we have nothing to flush.
+ if (size == 0)
+ {
+ return;
+ }
+
// There is a small gap here where the action is removed but _actionRegistered is still 1.
// In this case it will skip registering the action, but here we are already handling it,
// so there shouldn't be any issue as it's the same handler for all actions.
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
index 84171c7a9..72b743906 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
@@ -152,6 +152,32 @@ namespace Ryujinx.Graphics.Gpu.Image
// Linear textures are presumed to be used for readback initially.
_flushBalance = FlushBalanceThreshold + FlushBalanceIncrement;
}
+
+ foreach (RegionHandle handle in handles)
+ {
+ handle.RegisterDirtyEvent(DirtyAction);
+ }
+ }
+
+ ///
+ /// The action to perform when a memory tracking handle is flipped to dirty.
+ /// This notifies overlapping textures that the memory needs to be synchronized.
+ ///
+ private void DirtyAction()
+ {
+ // Notify all textures that belong to this handle.
+
+ _group.Storage.SignalGroupDirty();
+
+ lock (Overlaps)
+ {
+ foreach (Texture overlap in Overlaps)
+ {
+ overlap.SignalGroupDirty();
+ }
+ }
+
+ DeferredCopy = null;
}
///
@@ -449,7 +475,6 @@ namespace Ryujinx.Graphics.Gpu.Image
public void DeferCopy(TextureGroupHandle copyFrom)
{
Modified = false;
-
DeferredCopy = copyFrom;
_group.Storage.SignalGroupDirty();
@@ -506,7 +531,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
existing.Other.Handle.CreateCopyDependency(this);
- if (copyToOther)
+ if (copyToOther && Modified)
{
existing.Other.Handle.DeferCopy(this);
}
@@ -550,10 +575,10 @@ namespace Ryujinx.Graphics.Gpu.Image
if (fromHandle != null)
{
// Only copy if the copy texture is still modified.
- // It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush.
+ // DeferredCopy will be set to null if new data is written from CPU (see the DirtyAction method).
// It will also set as unmodified if a copy is deferred to it.
- shouldCopy = fromHandle.Modified;
+ shouldCopy = true;
if (fromHandle._bindCount == 0)
{
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
index ed181640a..fa278bbd9 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
@@ -20,8 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly Texture[] _rtColors;
private readonly ITexture[] _rtHostColors;
+ private readonly bool[] _rtColorsBound;
private Texture _rtDepthStencil;
private ITexture _rtHostDs;
+ private bool _rtDsBound;
public int ClipRegionWidth { get; private set; }
public int ClipRegionHeight { get; private set; }
@@ -51,6 +53,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_rtColors = new Texture[Constants.TotalRenderTargets];
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
+ _rtColorsBound = new bool[Constants.TotalRenderTargets];
}
///
@@ -154,7 +157,14 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_rtColors[index] != color)
{
- _rtColors[index]?.SignalModifying(false);
+ if (_rtColorsBound[index])
+ {
+ _rtColors[index]?.SignalModifying(false);
+ }
+ else
+ {
+ _rtColorsBound[index] = true;
+ }
if (color != null)
{
@@ -180,7 +190,14 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_rtDepthStencil != depthStencil)
{
- _rtDepthStencil?.SignalModifying(false);
+ if (_rtDsBound)
+ {
+ _rtDepthStencil?.SignalModifying(false);
+ }
+ else
+ {
+ _rtDsBound = true;
+ }
if (depthStencil != null)
{
@@ -413,21 +430,45 @@ namespace Ryujinx.Graphics.Gpu.Image
{
bool anyChanged = false;
- if (_rtHostDs != _rtDepthStencil?.HostTexture)
- {
- _rtHostDs = _rtDepthStencil?.HostTexture;
+ Texture dsTexture = _rtDepthStencil;
+ ITexture hostDsTexture = null;
+ if (dsTexture != null)
+ {
+ hostDsTexture = dsTexture.HostTexture;
+
+ if (!_rtDsBound)
+ {
+ dsTexture.SignalModifying(true);
+ _rtDsBound = true;
+ }
+ }
+
+ if (_rtHostDs != hostDsTexture)
+ {
+ _rtHostDs = hostDsTexture;
anyChanged = true;
}
for (int index = 0; index < _rtColors.Length; index++)
{
- ITexture hostTexture = _rtColors[index]?.HostTexture;
+ Texture texture = _rtColors[index];
+ ITexture hostTexture = null;
+
+ if (texture != null)
+ {
+ hostTexture = texture.HostTexture;
+
+ if (!_rtColorsBound[index])
+ {
+ texture.SignalModifying(true);
+ _rtColorsBound[index] = true;
+ }
+ }
if (_rtHostColors[index] != hostTexture)
{
_rtHostColors[index] = hostTexture;
-
anyChanged = true;
}
}
@@ -452,6 +493,31 @@ namespace Ryujinx.Graphics.Gpu.Image
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
}
+ ///
+ /// Marks all currently bound render target textures as modified, and also makes them be set as modified again on next use.
+ ///
+ public void RefreshModifiedTextures()
+ {
+ Texture dsTexture = _rtDepthStencil;
+
+ if (dsTexture != null && _rtDsBound)
+ {
+ dsTexture.SignalModifying(false);
+ _rtDsBound = false;
+ }
+
+ for (int index = 0; index < _rtColors.Length; index++)
+ {
+ Texture texture = _rtColors[index];
+
+ if (texture != null && _rtColorsBound[index])
+ {
+ texture.SignalModifying(false);
+ _rtColorsBound[index] = false;
+ }
+ }
+ }
+
///
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
///
@@ -488,11 +554,19 @@ namespace Ryujinx.Graphics.Gpu.Image
for (int i = 0; i < _rtColors.Length; i++)
{
- _rtColors[i]?.DecrementReferenceCount();
+ if (_rtColorsBound[i])
+ {
+ _rtColors[i]?.DecrementReferenceCount();
+ }
+
_rtColors[i] = null;
}
- _rtDepthStencil?.DecrementReferenceCount();
+ if (_rtDsBound)
+ {
+ _rtDepthStencil?.DecrementReferenceCount();
+ }
+
_rtDepthStencil = null;
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
index 0fdb6cd64..a4035577d 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
@@ -430,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo))
{
- if (gpuVa != 0 && (int)format > 0)
+ if (gpuVa != 0 && format != 0)
{
Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb}).");
}
diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
index 5255a6e00..6f1cce6ac 100644
--- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
+++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 43fad9f8b..dd25dd0f8 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
- private const uint CodeGenVersion = 5767;
+ private const uint CodeGenVersion = 5958;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index 9d030cd60..a5b31363b 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -186,6 +186,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat;
+ public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets;
+
public bool QueryHostSupportsTextureShadowLod() => _context.Capabilities.SupportsTextureShadowLod;
public bool QueryHostSupportsTransformFeedback() => _context.Capabilities.SupportsTransformFeedback;
diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs
index ccec763e3..2d5eede58 100644
--- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs
@@ -37,10 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// The incremented value of the syncpoint
public uint IncrementSyncpoint(uint id)
{
- if (id >= MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].Increment();
}
@@ -53,10 +50,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// The value of the syncpoint
public uint GetSyncpointValue(uint id)
{
- if (id >= MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].Value;
}
@@ -72,10 +66,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// The created SyncpointWaiterHandle object or null if already past threshold
public SyncpointWaiterHandle RegisterCallbackOnSyncpoint(uint id, uint threshold, Action callback)
{
- if (id >= MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].RegisterCallback(threshold, callback);
}
@@ -88,10 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// Thrown when id >= MaxHardwareSyncpoints
public void UnregisterCallback(uint id, SyncpointWaiterHandle waiterInformation)
{
- if (id >= MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
_syncpoints[id].UnregisterCallback(waiterInformation);
}
@@ -107,10 +95,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// True if timed out
public bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout)
{
- if (id >= MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
// TODO: Remove this when GPU channel scheduling will be implemented.
if (timeout == Timeout.InfiniteTimeSpan)
diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
index 3cff4061e..22959fad8 100644
--- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
+++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
index bff1e803b..d1a6358c2 100644
--- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
+++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
index bff1e803b..d1a6358c2 100644
--- a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
+++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
index bfba98a73..fd49a7c80 100644
--- a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
+++ b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
index e33940cb1..128f481f6 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
@@ -367,7 +367,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
return to;
}
- private TextureView PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
+ public void PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
{
int dstWidth = width;
int dstHeight = height;
@@ -445,8 +445,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
}
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
-
- return to;
}
private void EnsurePbo(TextureView view)
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 0f5fe46a5..7f1b1c382 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -140,6 +140,28 @@ namespace Ryujinx.Graphics.OpenGL.Image
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, 0, firstLayer, 0, firstLevel, layers, levels);
}
+ else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
+ {
+ int layers = Math.Min(Info.GetLayers(), destinationView.Info.GetLayers() - firstLayer);
+ int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
+
+ for (int level = 0; level < levels; level++)
+ {
+ int srcWidth = Math.Max(1, Width >> level);
+ int srcHeight = Math.Max(1, Height >> level);
+
+ int dstWidth = Math.Max(1, destinationView.Width >> (firstLevel + level));
+ int dstHeight = Math.Max(1, destinationView.Height >> (firstLevel + level));
+
+ int minWidth = Math.Min(srcWidth, dstWidth);
+ int minHeight = Math.Min(srcHeight, dstHeight);
+
+ for (int layer = 0; layer < layers; layer++)
+ {
+ _renderer.TextureCopy.PboCopy(this, destinationView, 0, firstLayer + layer, 0, firstLevel + level, minWidth, minHeight);
+ }
+ }
+ }
else
{
_renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel);
@@ -169,6 +191,13 @@ namespace Ryujinx.Graphics.OpenGL.Image
{
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
}
+ else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
+ {
+ int minWidth = Math.Min(Width, destinationView.Width);
+ int minHeight = Math.Min(Height, destinationView.Height);
+
+ _renderer.TextureCopy.PboCopy(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, minWidth, minHeight);
+ }
else
{
_renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 060ab815b..bd2f0a84f 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -163,6 +163,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
supportsShaderBarrierDivergence: !(intelWindows || intelUnix),
supportsShaderFloat64: true,
+ supportsTextureGatherOffsets: true,
supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod,
supportsVertexStoreAndAtomics: true,
supportsViewportIndexVertexTessellation: HwCapabilities.SupportsShaderViewportLayerArray,
diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
index 2313cc68f..3d64da99b 100644
--- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
+++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index 4dc75a3e1..29a5435e3 100644
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -339,6 +339,15 @@ namespace Ryujinx.Graphics.Shader
return true;
}
+ ///
+ /// Queries host GPU texture gather with multiple offsets support.
+ ///
+ /// True if the GPU and driver supports texture gather offsets, false otherwise
+ bool QueryHostSupportsTextureGatherOffsets()
+ {
+ return true;
+ }
+
///
/// Queries host GPU texture shadow LOD support.
///
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index 5a231079a..55f7d5778 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset;
}
- sourcesList.Add(Const((int)component));
+ if (!hasDepthCompare)
+ {
+ sourcesList.Add(Const((int)component));
+ }
Operand[] sources = sourcesList.ToArray();
Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)];
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs
index 6c95c7bdd..f4c4fef42 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs
@@ -69,10 +69,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
public Operand GetDest(int index)
{
- if (index != 0)
- {
- throw new ArgumentOutOfRangeException(nameof(index));
- }
+ ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0);
return _dest;
}
diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index ea9a7821b..8ccf5348f 100644
--- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 19b7999a7..6706053e5 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -29,7 +29,16 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery())
{
- Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block);
+ Operand bindlessHandle = texOp.GetSource(0);
+
+ // In some cases the compiler uses a shuffle operation to get the handle,
+ // for some textureGrad implementations. In those cases, we can skip the shuffle.
+ if (bindlessHandle.AsgOp is Operation shuffleOp && shuffleOp.Inst == Instruction.Shuffle)
+ {
+ bindlessHandle = shuffleOp.GetSource(0);
+ }
+
+ bindlessHandle = Utils.FindLastOperation(bindlessHandle, block);
// Some instructions do not encode an accurate sampler type:
// - Most instructions uses the same type for 1D and Buffer.
@@ -55,7 +64,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
continue;
}
- if (bindlessHandle.AsgOp is not Operation handleCombineOp)
+ if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp))
{
continue;
}
@@ -199,9 +208,64 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
}
}
+ private static bool TryGetOperation(INode asgOp, out Operation outOperation)
+ {
+ if (asgOp is PhiNode phi)
+ {
+ // If we have a phi, let's check if all inputs are effectively the same value.
+ // If so, we can "see through" the phi and pick any of the inputs (since they are all the same).
+
+ Operand firstSrc = phi.GetSource(0);
+
+ for (int index = 1; index < phi.SourcesCount; index++)
+ {
+ if (!IsSameOperand(firstSrc, phi.GetSource(index)))
+ {
+ outOperation = null;
+
+ return false;
+ }
+ }
+
+ asgOp = firstSrc.AsgOp;
+ }
+
+ if (asgOp is Operation operation)
+ {
+ outOperation = operation;
+
+ return true;
+ }
+
+ outOperation = null;
+
+ return false;
+ }
+
+ private static bool IsSameOperand(Operand x, Operand y)
+ {
+ if (x.Type == y.Type && x.Type == OperandType.LocalVariable)
+ {
+ return x.AsgOp is Operation xOp &&
+ y.AsgOp is Operation yOp &&
+ xOp.Inst == Instruction.BitwiseOr &&
+ yOp.Inst == Instruction.BitwiseOr &&
+ AreBothEqualConstantBuffers(xOp.GetSource(0), yOp.GetSource(0)) &&
+ AreBothEqualConstantBuffers(xOp.GetSource(1), yOp.GetSource(1));
+ }
+
+ return false;
+ }
+
+ private static bool AreBothEqualConstantBuffers(Operand x, Operand y)
+ {
+ return x.Type == y.Type && x.Value == y.Value && x.Type == OperandType.ConstantBuffer;
+ }
+
private static Operand GetSourceForMaskedHandle(Operation asgOp, uint mask)
{
// Assume it was already checked that the operation is bitwise AND.
+
Operand src0 = asgOp.GetSource(0);
Operand src1 = asgOp.GetSource(1);
@@ -210,6 +274,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
// We can't check if the mask matches here as both operands are from a constant buffer.
// Be optimistic and assume it matches. Avoid constant buffer 1 as official drivers
// uses this one to store compiler constants.
+
return src0.GetCbufSlot() == 1 ? src1 : src0;
}
else if (src0.Type == OperandType.ConstantBuffer && src1.Type == OperandType.Constant)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
index dbfe6269e..495ea8a94 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
@@ -303,7 +303,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
bool hasOffset = (texOp.Flags & TextureFlags.Offset) != 0;
bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0;
- bool hasInvalidOffset = (hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset();
+ bool needsOffsetsEmulation = hasOffsets && !gpuAccessor.QueryHostSupportsTextureGatherOffsets();
+
+ bool hasInvalidOffset = needsOffsetsEmulation || ((hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset());
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
@@ -402,11 +404,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
offsets[index] = offset;
}
- hasInvalidOffset &= !areAllOffsetsConstant;
-
- if (!hasInvalidOffset)
+ if (!needsOffsetsEmulation)
{
- return node;
+ hasInvalidOffset &= !areAllOffsetsConstant;
+
+ if (!hasInvalidOffset)
+ {
+ return node;
+ }
}
if (hasLodBias)
@@ -434,13 +439,13 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
LinkedListNode oldNode = node;
- if (isGather && !isShadow)
+ if (isGather && !isShadow && hasOffsets)
{
Operand[] newSources = new Operand[sources.Length];
sources.CopyTo(newSources, 0);
- Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
+ Operand[] texSizes = InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount);
int destIndex = 0;
@@ -455,7 +460,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
{
Operand offset = Local();
- Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)];
+ Operand intOffset = offsets[index + compIndex * coordsCount];
node.List.AddBefore(node, new Operation(
Instruction.FP32 | Instruction.Divide,
@@ -478,7 +483,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Format,
texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets),
texOp.Binding,
- 1,
+ 1 << 3, // W component: i=0, j=0
new[] { dests[destIndex++] },
newSources);
@@ -502,7 +507,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
else
{
- Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
+ Operand[] texSizes = isGather
+ ? InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount)
+ : InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
for (int index = 0; index < coordsCount; index++)
{
@@ -549,6 +556,43 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
return node;
}
+ private static Operand[] InsertTextureBaseSize(
+ LinkedListNode node,
+ TextureOperation texOp,
+ Operand bindlessHandle,
+ int coordsCount)
+ {
+ Operand[] texSizes = new Operand[coordsCount];
+
+ for (int index = 0; index < coordsCount; index++)
+ {
+ texSizes[index] = Local();
+
+ Operand[] texSizeSources;
+
+ if (bindlessHandle != null)
+ {
+ texSizeSources = new Operand[] { bindlessHandle, Const(0) };
+ }
+ else
+ {
+ texSizeSources = new Operand[] { Const(0) };
+ }
+
+ node.List.AddBefore(node, new TextureOperation(
+ Instruction.TextureQuerySize,
+ texOp.Type,
+ texOp.Format,
+ texOp.Flags,
+ texOp.Binding,
+ index,
+ new[] { texSizes[index] },
+ texSizeSources));
+ }
+
+ return texSizes;
+ }
+
private static Operand[] InsertTextureLod(
LinkedListNode node,
TextureOperation texOp,
diff --git a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
index 70e3453c3..51721490e 100644
--- a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
+++ b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj
@@ -1,6 +1,6 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs
index de7c9262d..8ca3f89bc 100644
--- a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs
+++ b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
@@ -102,11 +103,11 @@ namespace Ryujinx.Graphics.Texture.Utils
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static RgbaColor32 operator <<(RgbaColor32 x, int shift)
+ public static RgbaColor32 operator <<(RgbaColor32 x, [ConstantExpected] byte shift)
{
if (Sse2.IsSupported)
{
- return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, (byte)shift));
+ return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, shift));
}
else
{
@@ -115,11 +116,11 @@ namespace Ryujinx.Graphics.Texture.Utils
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static RgbaColor32 operator >>(RgbaColor32 x, int shift)
+ public static RgbaColor32 operator >>(RgbaColor32 x, [ConstantExpected] byte shift)
{
if (Sse2.IsSupported)
{
- return new RgbaColor32(Sse2.ShiftRightLogical(x._color, (byte)shift));
+ return new RgbaColor32(Sse2.ShiftRightLogical(x._color, shift));
}
else
{
diff --git a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
index 2a7cdd985..cfebcfa2a 100644
--- a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
+++ b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
index 9cf37670e..abff58a53 100644
--- a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
+++ b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 156b3db16..7346d7891 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -189,7 +189,7 @@ namespace Ryujinx.Graphics.Vulkan
PipelineStageFlags.AllCommandsBit,
0,
1,
- new ReadOnlySpan(memoryBarrier),
+ new ReadOnlySpan(in memoryBarrier),
0,
ReadOnlySpan.Empty,
0,
diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
index 8d30457e2..f6a7be91e 100644
--- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
+++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
index 09128f007..05dbd15ce 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
@@ -211,6 +211,13 @@ namespace Ryujinx.Graphics.Vulkan
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels);
}
+ else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
+ {
+ int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
+ int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
+
+ _gd.HelperShader.CopyColor(_gd, cbs, src, dst, 0, firstLayer, 0, FirstLevel, layers, levels);
+ }
else
{
TextureCopy.Copy(
@@ -260,6 +267,10 @@ namespace Ryujinx.Graphics.Vulkan
{
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
}
+ else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
+ {
+ _gd.HelperShader.CopyColor(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
+ }
else
{
TextureCopy.Copy(
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs
index 983f03d4e..2d9dbc348 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs
@@ -33,9 +33,5 @@ namespace Ryujinx.Graphics.Vulkan
public VulkanException(string message, Exception innerException) : base(message, innerException)
{
}
-
- protected VulkanException(SerializationInfo info, StreamingContext context) : base(info, context)
- {
- }
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 0b824d7c3..ed7c1faaa 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -605,6 +605,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsShaderBallot: false,
supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
supportsShaderFloat64: Capabilities.SupportsShaderFloat64,
+ supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk,
supportsTextureShadowLod: false,
supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics,
supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,
diff --git a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs
index e43c838a2..9cb1cf2c7 100644
--- a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs
+++ b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs
@@ -35,8 +35,6 @@ namespace Ryujinx.HLE.Exceptions
Request = context.Request;
}
- protected ServiceNotImplementedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
-
public override string Message
{
get
diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
index 646808e78..b27eb5ead 100644
--- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs
+++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
@@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem
{
using var ncaFile = new UniqueRef();
- fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read);
+ fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType != NcaContentType.Meta)
{
@@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem
using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
using var cnmtFile = new UniqueRef();
- pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read);
+ pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
var cnmt = new Cnmt(cnmtFile.Get.AsStream());
if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
@@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem
string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
- AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true);
+ AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
}
}
@@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem
if (!mergedToContainer)
{
using FileStream fileStream = File.OpenRead(containerPath);
- using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage());
+ using PartitionFileSystem partitionFileSystem = new();
+ partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(partitionFileSystem);
}
@@ -259,17 +260,17 @@ namespace Ryujinx.HLE.FileSystem
{
var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
using var ncaFile = new UniqueRef();
- PartitionFileSystem pfs;
switch (Path.GetExtension(aoc.ContainerPath))
{
case ".xci":
- pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
- pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
+ var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
+ xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
break;
case ".nsp":
- pfs = new PartitionFileSystem(file.AsStorage());
- pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(file.AsStorage());
+ pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
break;
default:
return false; // Print error?
@@ -419,10 +420,7 @@ namespace Ryujinx.HLE.FileSystem
if (locationList != null)
{
- if (locationList.Contains(entry))
- {
- locationList.Remove(entry);
- }
+ locationList.Remove(entry);
locationList.AddLast(entry);
}
@@ -606,11 +604,11 @@ namespace Ryujinx.HLE.FileSystem
if (filesystem.FileExists($"{path}/00"))
{
- filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode);
+ filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure();
}
else
{
- filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode);
+ filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure();
}
return file.Release();
diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
index 807020c60..43bd27761 100644
--- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
+++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
@@ -7,6 +7,7 @@ using LibHac.Fs.Shim;
using LibHac.FsSrv;
using LibHac.FsSystem;
using LibHac.Ncm;
+using LibHac.Sdmmc;
using LibHac.Spl;
using LibHac.Tools.Es;
using LibHac.Tools.Fs;
@@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem
public KeySet KeySet { get; private set; }
public EmulatedGameCard GameCard { get; private set; }
- public EmulatedSdCard SdCard { get; private set; }
+ public SdmmcApi SdCard { get; private set; }
public ModLoader ModLoader { get; private set; }
private readonly ConcurrentDictionary _romFsByPid;
@@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem
fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
GameCard = fsServerObjects.GameCard;
- SdCard = fsServerObjects.SdCard;
+ SdCard = fsServerObjects.Sdmmc;
- SdCard.SetSdCardInsertionStatus(true);
+ SdCard.SetSdCardInserted(true);
var fsServerConfig = new FileSystemServerConfig
{
- DeviceOperator = fsServerObjects.DeviceOperator,
ExternalKeySet = KeySet.ExternalKeySet,
FsCreators = fsServerObjects.FsCreators,
+ StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory,
RandomGenerator = randomGenerator,
};
@@ -263,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem
if (result.IsSuccess())
{
- Ticket ticket = new(ticketFile.Get.AsStream());
+ // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle
+ // of the hashed portion (usually the first 0x200 bytes) of the file and end the read after
+ // the end of the hashed portion, so we read the ticket file using a single read.
+ byte[] ticketData = new byte[0x2C0];
+ result = ticketFile.Get.Read(out long bytesRead, 0, ticketData);
+
+ if (result.IsFailure() || bytesRead != ticketData.Length)
+ continue;
+
+ Ticket ticket = new(new MemoryStream(ticketData));
var titleKey = ticket.GetTitleKey(KeySet);
if (titleKey != null)
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index b1ba11b59..d52f1815a 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -101,7 +101,7 @@ namespace Ryujinx.HLE
///
/// Control if the guest application should be told that there is a Internet connection available.
///
- internal readonly bool EnableInternetAccess;
+ public bool EnableInternetAccess { internal get; set; }
///
/// Control LibHac's integrity check level.
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Common/KTimeManager.cs b/src/Ryujinx.HLE/HOS/Kernel/Common/KTimeManager.cs
index 499bc2c61..3c5fa067f 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Common/KTimeManager.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Common/KTimeManager.cs
@@ -1,4 +1,5 @@
using Ryujinx.Common;
+using Ryujinx.Common.PreciseSleep;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -23,7 +24,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
private readonly KernelContext _context;
private readonly List _waitingObjects;
- private AutoResetEvent _waitEvent;
+ private IPreciseSleepEvent _waitEvent;
private bool _keepRunning;
private long _enforceWakeupFromSpinWait;
@@ -54,6 +55,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
timePoint = long.MaxValue;
}
+ timePoint = _waitEvent.AdjustTimePoint(timePoint, timeout);
+
lock (_context.CriticalSection.Lock)
{
_waitingObjects.Add(new WaitingObject(schedulerObj, timePoint));
@@ -64,7 +67,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
}
}
- _waitEvent.Set();
+ _waitEvent.Signal();
}
public void UnscheduleFutureInvocation(IKFutureSchedulerObject schedulerObj)
@@ -83,10 +86,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
private void WaitAndCheckScheduledObjects()
{
- SpinWait spinWait = new();
WaitingObject next;
- using (_waitEvent = new AutoResetEvent(false))
+ using (_waitEvent = PreciseSleepHelper.CreateEvent())
{
while (_keepRunning)
{
@@ -103,30 +105,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
if (next.TimePoint > timePoint)
{
- long ms = Math.Min((next.TimePoint - timePoint) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
-
- if (ms > 0)
+ if (!_waitEvent.SleepUntil(next.TimePoint))
{
- _waitEvent.WaitOne((int)ms);
- }
- else
- {
- while (Interlocked.Read(ref _enforceWakeupFromSpinWait) != 1 && PerformanceCounter.ElapsedTicks < next.TimePoint)
- {
- // Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
- if (spinWait.NextSpinWillYield)
- {
- Thread.Yield();
-
- spinWait.Reset();
- }
- else
- {
- spinWait.SpinOnce();
- }
- }
-
- spinWait.Reset();
+ PreciseSleepHelper.SpinWaitUntilTimePoint(next.TimePoint, ref _enforceWakeupFromSpinWait);
}
}
@@ -145,7 +126,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
}
else
{
- _waitEvent.WaitOne();
+ _waitEvent.Sleep();
}
}
}
@@ -212,7 +193,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
public void Dispose()
{
_keepRunning = false;
- _waitEvent?.Set();
+ _waitEvent?.Signal();
}
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
new file mode 100644
index 000000000..32734574e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
@@ -0,0 +1,46 @@
+using Ryujinx.Memory;
+using System;
+
+namespace Ryujinx.HLE.HOS.Kernel.Memory
+{
+ [Flags]
+ enum KMemoryPermission : uint
+ {
+ None = 0,
+ UserMask = Read | Write | Execute,
+ Mask = uint.MaxValue,
+
+ Read = 1 << 0,
+ Write = 1 << 1,
+ Execute = 1 << 2,
+ DontCare = 1 << 28,
+
+ ReadAndWrite = Read | Write,
+ ReadAndExecute = Read | Execute,
+ }
+
+ static class KMemoryPermissionExtensions
+ {
+ public static MemoryPermission Convert(this KMemoryPermission permission)
+ {
+ MemoryPermission output = MemoryPermission.None;
+
+ if (permission.HasFlag(KMemoryPermission.Read))
+ {
+ output = MemoryPermission.Read;
+ }
+
+ if (permission.HasFlag(KMemoryPermission.Write))
+ {
+ output |= MemoryPermission.Write;
+ }
+
+ if (permission.HasFlag(KMemoryPermission.Execute))
+ {
+ output |= MemoryPermission.Execute;
+ }
+
+ return output;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs
index dcfc8f4ff..4cd3e6fdd 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs
@@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
///
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
{
- // TODO.
+ _cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert());
+
return Result.Success;
}
///
- protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission)
+ protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission)
{
- // TODO.
- return Result.Success;
+ // TODO: Flush JIT cache.
+
+ return Reprotect(address, pagesCount, permission);
}
///
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
index 2b00f802a..2b6d4e4e9 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs
@@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
if ((oldPermission & KMemoryPermission.Execute) != 0)
{
- result = ReprotectWithAttributes(address, pagesCount, permission);
+ result = ReprotectAndFlush(address, pagesCount, permission);
}
else
{
@@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
///
- /// Changes the permissions of a given virtual memory region.
+ /// Changes the permissions of a given virtual memory region, while also flushing the cache.
///
/// Virtual address of the region to have the permission changes
/// Number of pages to have their permissions changed
/// New permission
/// Result of the permission change operation
- protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission);
+ protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission);
///
/// Alerts the memory tracking that a given region has been read from or written to.
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs
deleted file mode 100644
index 068cdbb88..000000000
--- a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-
-namespace Ryujinx.HLE.HOS.Kernel.Memory
-{
- [Flags]
- enum KMemoryPermission : uint
- {
- None = 0,
- UserMask = Read | Write | Execute,
- Mask = uint.MaxValue,
-
- Read = 1 << 0,
- Write = 1 << 1,
- Execute = 1 << 2,
- DontCare = 1 << 28,
-
- ReadAndWrite = Read | Write,
- ReadAndExecute = Read | Execute,
- }
-}
diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs
index 6706006c3..834bc0595 100644
--- a/src/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/src/Ryujinx.HLE/HOS/ModLoader.cs
@@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS
Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
- exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage());
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure();
+ exefs = pfs;
return true;
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
index 599025e3b..1ef52a00d 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
@@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
try
{
LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
- using SharedRef nsp = new(new PartitionFileSystem(storage));
+ var pfs = new PartitionFileSystem();
+ using SharedRef nsp = new(pfs);
+ pfs.Initialize(storage).ThrowIfFailure();
ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet);
@@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
try
{
- PartitionFileSystem nsp = new(pfsFile.AsStorage());
+ PartitionFileSystem nsp = new();
+ nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure();
ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
index 4c5c56240..66020d57b 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
@@ -1,6 +1,7 @@
using LibHac;
using LibHac.Common;
using LibHac.Fs;
+using LibHac.Fs.Fsa;
using Path = LibHac.FsSrv.Sf.Path;
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
@@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
return (ResultCode)result.Value;
}
+ [CommandCmif(16)]
+ public ResultCode GetFileSystemAttribute(ServiceCtx context)
+ {
+ Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute);
+
+ context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute));
+
+ return (ResultCode)result.Value;
+ }
+
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
index 644e1a17a..24dd1e9be 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
@@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs
[CommandCmif(1016)]
public ResultCode FlushAccessLogOnSdCard(ServiceCtx context)
{
- return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
+ // Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since
+ // there's nothing to flush. Return success until it's implemented.
+ // return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
+ return ResultCode.Success;
}
[CommandCmif(1017)]
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
new file mode 100644
index 000000000..80ea2c9d7
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn
+{
+ static class LdnConst
+ {
+ public const int SsidLengthMax = 0x20;
+ public const int AdvertiseDataSizeMax = 0x180;
+ public const int UserNameBytesMax = 0x20;
+ public const int NodeCountMax = 8;
+ public const int StationCountMax = NodeCountMax - 1;
+ public const int PassphraseLengthMax = 0x40;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
index 4b7241c43..5fb2aca05 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
index c57a7dc45..9d5477931 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
index f33ceaebe..0461e783e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
@@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
result[i].Reserved = new Array7();
- if (i < 8)
+ if (i < LdnConst.NodeCountMax)
{
result[i].State = array[i].State;
array[i].State = NodeLatestUpdateFlags.None;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
index 85a19a875..5939a1394 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
index 72db4d41a..764862508 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
index 1401f5214..3820f936e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
index 07bbbeda3..78ebcac82 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange -= NetworkChanged;
}
- private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
similarity index 81%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
index ff342d27c..81825e977 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
@@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
+ bool NeedsRealId { get; }
+
event EventHandler NetworkChange;
void DisconnectNetwork();
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index 29cc0e1b9..8c6ea66f7 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -8,7 +8,7 @@ using Ryujinx.Cpu;
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.RyuLdn;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
@@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
- if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
+ if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
NetworkConfig networkConfig = context.RequestData.ReadStruct();
- if (networkConfig.IntentId.LocalCommunicationId == -1)
+ if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
- if (!isLocalCommunicationIdValid)
+ if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
return ResultCode.InvalidObject;
}
@@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
- if (networkConfig.NodeCountMax <= 8)
+ if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax)
{
if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
{
if (securityConfig.SecurityMode <= SecurityMode.Retail)
{
- if (securityConfig.Passphrase.Length <= 0x40)
+ if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax)
{
if (_state == NetworkState.AccessPoint)
{
@@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
return _nifmResultCode;
}
- if (bufferSize == 0 || bufferSize > 0x180)
+ if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax)
{
return ResultCode.InvalidArgument;
}
@@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.Memory.Read(bufferPosition, networkInfoBytes);
- networkInfo = MemoryMarshal.Cast(networkInfoBytes)[0];
+ networkInfo = MemoryMarshal.Read(networkInfoBytes);
}
- if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
+ if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
- if (!isLocalCommunicationIdValid)
+ if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
return ResultCode.InvalidObject;
}
@@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
{
MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}");
+
switch (mode)
{
+ case MultiplayerMode.LdnMitm:
+ NetworkClient = new LdnMitmClient(context.Device.Configuration);
+ break;
case MultiplayerMode.Disabled:
- NetworkClient = new DisabledLdnClient();
+ NetworkClient = new LdnDisabledClient();
break;
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
similarity index 87%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
index 75a1e35ff..e5340b4e9 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
@@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
- class DisabledLdnClient : INetworkClient
+ class LdnDisabledClient : INetworkClient
{
+ public bool NeedsRealId => true;
+
public event EventHandler NetworkChange;
public NetworkError Connect(ConnectRequest request)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
new file mode 100644
index 000000000..8cfd77acb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
@@ -0,0 +1,611 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ internal class LanDiscovery : IDisposable
+ {
+ private const int DefaultPort = 11452;
+ private const ushort CommonChannel = 6;
+ private const byte CommonLinkLevel = 3;
+ private const byte CommonNetworkType = 2;
+
+ private const int FailureTimeout = 4000;
+
+ private readonly LdnMitmClient _parent;
+ private readonly LanProtocol _protocol;
+ private bool _initialized;
+ private readonly Ssid _fakeSsid;
+ private ILdnTcpSocket _tcp;
+ private LdnProxyUdpServer _udp, _udp2;
+ private readonly List _stations = new();
+ private readonly object _lock = new();
+
+ private readonly AutoResetEvent _apConnected = new(false);
+
+ internal readonly IPAddress LocalAddr;
+ internal readonly IPAddress LocalBroadcastAddr;
+ internal NetworkInfo NetworkInfo;
+
+ public bool IsHost => _tcp is LdnProxyTcpServer;
+
+ private readonly Random _random = new();
+
+ // NOTE: Credit to https://stackoverflow.com/a/39338188
+ private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
+ {
+ uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
+ uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
+ uint broadCastIpAddress = ipAddress | ~ipMaskV4;
+
+ return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
+ }
+
+ private static NetworkInfo GetEmptyNetworkInfo()
+ {
+ NetworkInfo networkInfo = new()
+ {
+ NetworkId = new NetworkId
+ {
+ SessionId = new Array16(),
+ },
+ Common = new CommonNetworkInfo
+ {
+ MacAddress = new Array6(),
+ Ssid = new Ssid
+ {
+ Name = new Array33(),
+ },
+ },
+ Ldn = new LdnNetworkInfo
+ {
+ NodeCountMax = LdnConst.NodeCountMax,
+ SecurityParameter = new Array16(),
+ Nodes = new Array8(),
+ AdvertiseData = new Array384(),
+ Reserved4 = new Array140(),
+ },
+ };
+
+ for (int i = 0; i < LdnConst.NodeCountMax; i++)
+ {
+ networkInfo.Ldn.Nodes[i] = new NodeInfo
+ {
+ MacAddress = new Array6(),
+ UserName = new Array33(),
+ Reserved2 = new Array16(),
+ };
+ }
+
+ return networkInfo;
+ }
+
+ public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
+
+ _parent = parent;
+ LocalAddr = ipAddress;
+ LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
+
+ _fakeSsid = new Ssid
+ {
+ Length = LdnConst.SsidLengthMax,
+ };
+ _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
+
+ _protocol = new LanProtocol(this);
+ _protocol.Accept += OnConnect;
+ _protocol.SyncNetwork += OnSyncNetwork;
+ _protocol.DisconnectStation += DisconnectStation;
+
+ NetworkInfo = GetEmptyNetworkInfo();
+
+ ResetStations();
+
+ if (!InitUdp())
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
+
+ return;
+ }
+
+ _initialized = true;
+ }
+
+ protected void OnSyncNetwork(NetworkInfo info)
+ {
+ bool updated = false;
+
+ lock (_lock)
+ {
+ if (!NetworkInfo.Equals(info))
+ {
+ NetworkInfo = info;
+ updated = true;
+
+ Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
+ }
+ }
+
+ if (updated)
+ {
+ _parent.InvokeNetworkChange(info, true);
+ }
+
+ _apConnected.Set();
+ }
+
+ protected void OnConnect(LdnProxyTcpSession station)
+ {
+ lock (_lock)
+ {
+ station.NodeId = LocateEmptyNode();
+
+ if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
+ {
+ station.Disconnect();
+ station.Dispose();
+
+ return;
+ }
+
+ _stations.Add(station);
+
+ UpdateNodes();
+ }
+ }
+
+ public void DisconnectStation(LdnProxyTcpSession station)
+ {
+ if (!station.IsDisposed)
+ {
+ if (station.IsConnected)
+ {
+ station.Disconnect();
+ }
+
+ station.Dispose();
+ }
+
+ lock (_lock)
+ {
+ if (_stations.Remove(station))
+ {
+ NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
+ {
+ MacAddress = new Array6(),
+ UserName = new Array33(),
+ Reserved2 = new Array16(),
+ };
+
+ UpdateNodes();
+ }
+ }
+ }
+
+ public bool SetAdvertiseData(byte[] data)
+ {
+ if (data.Length > LdnConst.AdvertiseDataSizeMax)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
+
+ return false;
+ }
+
+ data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
+ NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
+
+ // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
+ lock (_lock)
+ {
+ if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
+ {
+ UpdateNodes(true);
+ }
+ }
+
+ return true;
+ }
+
+ public void InitNetworkInfo()
+ {
+ lock (_lock)
+ {
+ NetworkInfo.Common.MacAddress = GetFakeMac();
+ NetworkInfo.Common.Channel = CommonChannel;
+ NetworkInfo.Common.LinkLevel = CommonLinkLevel;
+ NetworkInfo.Common.NetworkType = CommonNetworkType;
+ NetworkInfo.Common.Ssid = _fakeSsid;
+
+ NetworkInfo.Ldn.Nodes = new Array8();
+
+ for (int i = 0; i < LdnConst.NodeCountMax; i++)
+ {
+ NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
+ NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
+ }
+ }
+ }
+
+ protected Array6 GetFakeMac(IPAddress address = null)
+ {
+ address ??= LocalAddr;
+
+ byte[] ip = address.GetAddressBytes();
+
+ var macAddress = new Array6();
+ new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
+
+ return macAddress;
+ }
+
+ public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
+ {
+ Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
+
+ if (_tcp != null)
+ {
+ _tcp.DisconnectAndStop();
+ _tcp.Dispose();
+ _tcp = null;
+ }
+
+ ILdnTcpSocket tcpSocket;
+
+ if (listening)
+ {
+ try
+ {
+ address ??= LocalAddr;
+
+ tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
+
+ return false;
+ }
+
+ if (!tcpSocket.Start())
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (address == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
+
+ return false;
+ }
+ }
+
+ _tcp = tcpSocket;
+
+ return true;
+ }
+
+ public bool InitUdp()
+ {
+ _udp?.Stop();
+ _udp2?.Stop();
+
+ try
+ {
+ // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
+ // Windows only works if bound to localhost or the local address.
+ // See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
+ }
+
+ _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
+ {
+ _udp.ClearScanResults();
+
+ if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
+ {
+ return Array.Empty();
+ }
+
+ List outNetworkInfo = new();
+
+ foreach (KeyValuePair item in _udp.GetScanResults())
+ {
+ bool copy = true;
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
+ {
+ copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
+ {
+ copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
+ {
+ copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
+ {
+ Span gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
+ Span scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
+ copy &= gameSsid.SequenceEqual(scanSsid);
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
+ {
+ copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
+ }
+
+ if (copy)
+ {
+ if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
+ {
+ outNetworkInfo.Add(item.Value);
+ }
+ else
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
+ }
+ }
+ }
+
+ return outNetworkInfo.ToArray();
+ }
+
+ protected void ResetStations()
+ {
+ lock (_lock)
+ {
+ foreach (LdnProxyTcpSession station in _stations)
+ {
+ station.Disconnect();
+ station.Dispose();
+ }
+
+ _stations.Clear();
+ }
+ }
+
+ private int LocateEmptyNode()
+ {
+ Array8 nodes = NetworkInfo.Ldn.Nodes;
+
+ for (int i = 1; i < nodes.Length; i++)
+ {
+ if (nodes[i].IsConnected == 0)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ protected void UpdateNodes(bool forceUpdate = false)
+ {
+ int countConnected = 1;
+
+ foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
+ {
+ countConnected++;
+
+ station.OverrideInfo();
+
+ // NOTE: This is not part of the original implementation.
+ NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
+ }
+
+ byte nodeCount = (byte)countConnected;
+
+ bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
+
+ NetworkInfo.Ldn.NodeCount = nodeCount;
+
+ foreach (LdnProxyTcpSession station in _stations)
+ {
+ if (station.IsConnected)
+ {
+ if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan(ref NetworkInfo).ToArray()) < 0)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
+ }
+ }
+ }
+
+ if (networkInfoChanged)
+ {
+ _parent.InvokeNetworkChange(NetworkInfo, true);
+ }
+ }
+
+ protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
+ {
+ uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
+
+ node.MacAddress = GetFakeMac();
+ node.IsConnected = 1;
+ node.UserName = userConfig.UserName;
+ node.LocalCommunicationVersion = localCommunicationVersion;
+ node.Ipv4Address = ipAddress;
+
+ return node;
+ }
+
+ public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
+ {
+ if (!InitTcp(true))
+ {
+ return false;
+ }
+
+ InitNetworkInfo();
+
+ NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
+ NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
+
+ NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
+
+ NetworkInfo.NetworkId.SessionId = new Array16();
+ _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
+ NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
+
+ NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
+ NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
+ NetworkInfo.Ldn.NodeCount++;
+
+ _parent.InvokeNetworkChange(NetworkInfo, true);
+
+ return true;
+ }
+
+ public void DestroyNetwork()
+ {
+ if (_tcp != null)
+ {
+ try
+ {
+ _tcp.DisconnectAndStop();
+ }
+ finally
+ {
+ _tcp.Dispose();
+ _tcp = null;
+ }
+ }
+
+ ResetStations();
+ }
+
+ public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
+ {
+ _apConnected.Reset();
+
+ if (networkInfo.Ldn.NodeCount == 0)
+ {
+ return NetworkError.Unknown;
+ }
+
+ IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
+
+ if (!InitTcp(false, address))
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
+
+ return NetworkError.ConnectNotFound;
+ }
+
+ if (!_tcp.Connect())
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
+
+ return NetworkError.ConnectFailure;
+ }
+
+ NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
+ if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan(ref myNode).ToArray()) < 0)
+ {
+ return NetworkError.Unknown;
+ }
+
+ return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
+ }
+
+ public void Dispose()
+ {
+ if (_initialized)
+ {
+ DisconnectAndStop();
+ ResetStations();
+ _initialized = false;
+ }
+
+ _protocol.Accept -= OnConnect;
+ _protocol.SyncNetwork -= OnSyncNetwork;
+ _protocol.DisconnectStation -= DisconnectStation;
+ }
+
+ public void DisconnectAndStop()
+ {
+ if (_udp != null)
+ {
+ try
+ {
+ _udp.Stop();
+ }
+ finally
+ {
+ _udp.Dispose();
+ _udp = null;
+ }
+ }
+
+ if (_udp2 != null)
+ {
+ try
+ {
+ _udp2.Stop();
+ }
+ finally
+ {
+ _udp2.Dispose();
+ _udp2 = null;
+ }
+ }
+
+ if (_tcp != null)
+ {
+ try
+ {
+ _tcp.DisconnectAndStop();
+ }
+ finally
+ {
+ _tcp.Dispose();
+ _tcp = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
new file mode 100644
index 000000000..f22e430bd
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
@@ -0,0 +1,314 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ internal class LanProtocol
+ {
+ private const uint LanMagic = 0x11451400;
+
+ public const int BufferSize = 2048;
+ public const int TcpTxBufferSize = 0x800;
+ public const int TcpRxBufferSize = 0x1000;
+ public const int TxBufferSizeMax = 0x2000;
+ public const int RxBufferSizeMax = 0x2000;
+
+ private readonly int _headerSize = Marshal.SizeOf();
+
+ private readonly LanDiscovery _discovery;
+
+ public event Action Accept;
+ public event Action Scan;
+ public event Action ScanResponse;
+ public event Action SyncNetwork;
+ public event Action Connect;
+ public event Action DisconnectStation;
+
+ public LanProtocol(LanDiscovery parent)
+ {
+ _discovery = parent;
+ }
+
+ public void InvokeAccept(LdnProxyTcpSession session)
+ {
+ Accept?.Invoke(session);
+ }
+
+ public void InvokeDisconnectStation(LdnProxyTcpSession session)
+ {
+ DisconnectStation?.Invoke(session);
+ }
+
+ private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null)
+ {
+ switch (header.Type)
+ {
+ case LanPacketType.Scan:
+ // UDP
+ if (_discovery.IsHost)
+ {
+ Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan(ref _discovery.NetworkInfo).ToArray());
+ }
+ break;
+ case LanPacketType.ScanResponse:
+ // UDP
+ ScanResponse?.Invoke(MemoryMarshal.Cast(data)[0]);
+ break;
+ case LanPacketType.SyncNetwork:
+ // TCP
+ SyncNetwork?.Invoke(MemoryMarshal.Cast(data)[0]);
+ break;
+ case LanPacketType.Connect:
+ // TCP Session / Station
+ Connect?.Invoke(MemoryMarshal.Cast(data)[0], endPoint);
+ break;
+ default:
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}");
+ break;
+ }
+ }
+
+ public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null)
+ {
+ if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address))
+ {
+ return;
+ }
+
+ int index = 0;
+ while (index < size)
+ {
+ if (bufferEnd < _headerSize)
+ {
+ int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd));
+
+ Array.Copy(data, index + offset, buffer, bufferEnd, copyable2);
+
+ index += copyable2;
+ bufferEnd += copyable2;
+ }
+
+ if (bufferEnd >= _headerSize)
+ {
+ LanPacketHeader header = MemoryMarshal.Cast(buffer)[0];
+ if (header.Magic != LanMagic)
+ {
+ bufferEnd = 0;
+
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]");
+
+ return;
+ }
+
+ int totalSize = _headerSize + header.Length;
+ if (totalSize > BufferSize)
+ {
+ bufferEnd = 0;
+
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded.");
+
+ return;
+ }
+
+ int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd));
+
+ Array.Copy(data, index + offset, buffer, bufferEnd, copyable);
+
+ index += copyable;
+ bufferEnd += copyable;
+
+ if (totalSize == bufferEnd)
+ {
+ byte[] ldnData = new byte[totalSize - _headerSize];
+ Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length);
+
+ if (header.Compressed == 1)
+ {
+ if (Decompress(ldnData, out byte[] decompressedLdnData) != 0)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}");
+
+ return;
+ }
+
+ if (decompressedLdnData.Length != header.DecompressLength)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})");
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'");
+
+ return;
+ }
+
+ ldnData = decompressedLdnData;
+ }
+
+ DecodeAndHandle(header, ldnData, endPoint);
+
+ bufferEnd = 0;
+ }
+ }
+ }
+ }
+
+ public int SendBroadcast(ILdnSocket s, LanPacketType type, int port)
+ {
+ return SendPacket(s, type, Array.Empty(), new IPEndPoint(_discovery.LocalBroadcastAddr, port));
+ }
+
+ public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null)
+ {
+ byte[] buf = PreparePacket(type, data);
+
+ return s.SendPacketAsync(endPoint, buf) ? 0 : -1;
+ }
+
+ public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data)
+ {
+ byte[] buf = PreparePacket(type, data);
+
+ return s.SendAsync(buf) ? 0 : -1;
+ }
+
+ private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type)
+ {
+ header.Magic = LanMagic;
+ header.Type = type;
+ header.Compressed = 0;
+ header.Length = 0;
+ header.DecompressLength = 0;
+ header.Reserved = new Array2();
+
+ return header;
+ }
+
+ private byte[] PreparePacket(LanPacketType type, byte[] data)
+ {
+ LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type);
+ header.Length = (ushort)data.Length;
+
+ byte[] buf;
+ if (data.Length > 0)
+ {
+ if (Compress(data, out byte[] compressed) == 0)
+ {
+ header.DecompressLength = header.Length;
+ header.Length = (ushort)compressed.Length;
+ header.Compressed = 1;
+
+ buf = new byte[compressed.Length + _headerSize];
+
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ compressed.CopyTo(buf, _headerSize);
+ }
+ else
+ {
+ buf = new byte[data.Length + _headerSize];
+
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed.");
+
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ data.CopyTo(buf, _headerSize);
+ }
+ }
+ else
+ {
+ buf = new byte[_headerSize];
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ }
+
+ return buf;
+ }
+
+ private int Compress(byte[] input, out byte[] output)
+ {
+ List outputList = new();
+ int i = 0;
+ int maxCount = 0xFF;
+
+ while (i < input.Length)
+ {
+ byte inputByte = input[i++];
+ int count = 0;
+
+ if (inputByte == 0)
+ {
+ while (i < input.Length && input[i] == 0 && count < maxCount)
+ {
+ count += 1;
+ i++;
+ }
+ }
+
+ if (inputByte == 0)
+ {
+ outputList.Add(0);
+
+ if (outputList.Count == BufferSize)
+ {
+ output = null;
+
+ return -1;
+ }
+
+ outputList.Add((byte)count);
+ }
+ else
+ {
+ outputList.Add(inputByte);
+ }
+ }
+
+ output = outputList.ToArray();
+
+ return i == input.Length ? 0 : -1;
+ }
+
+ private int Decompress(byte[] input, out byte[] output)
+ {
+ List outputList = new();
+ int i = 0;
+
+ while (i < input.Length && outputList.Count < BufferSize)
+ {
+ byte inputByte = input[i++];
+
+ outputList.Add(inputByte);
+
+ if (inputByte == 0)
+ {
+ if (i == input.Length)
+ {
+ output = null;
+
+ return -1;
+ }
+
+ int count = input[i++];
+
+ for (int j = 0; j < count; j++)
+ {
+ if (outputList.Count == BufferSize)
+ {
+ break;
+ }
+
+ outputList.Add(inputByte);
+ }
+ }
+ }
+
+ output = outputList.ToArray();
+
+ return i == input.Length ? 0 : -1;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
new file mode 100644
index 000000000..068013053
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Net.NetworkInformation;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ ///
+ /// Client implementation for ldn_mitm
+ ///
+ internal class LdnMitmClient : INetworkClient
+ {
+ public bool NeedsRealId => false;
+
+ public event EventHandler NetworkChange;
+
+ private readonly LanDiscovery _lanDiscovery;
+
+ public LdnMitmClient(HLEConfiguration config)
+ {
+ UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2;
+
+ _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask);
+ }
+
+ internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason));
+ }
+
+ public NetworkError Connect(ConnectRequest request)
+ {
+ return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion);
+ }
+
+ public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate");
+
+ return NetworkError.None;
+ }
+
+ public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+ {
+ return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig);
+ }
+
+ public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate");
+
+ return true;
+ }
+
+ public void DisconnectAndStop()
+ {
+ _lanDiscovery.DisconnectAndStop();
+ }
+
+ public void DisconnectNetwork()
+ {
+ _lanDiscovery.DestroyNetwork();
+ }
+
+ public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject");
+
+ return ResultCode.Success;
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+ {
+ return _lanDiscovery.Scan(channel, scanFilter);
+ }
+
+ public void SetAdvertiseData(byte[] data)
+ {
+ _lanDiscovery.SetAdvertiseData(data);
+ }
+
+ public void SetGameVersion(byte[] versionString)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion");
+ }
+
+ public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy");
+ }
+
+ public void Dispose()
+ {
+ _lanDiscovery.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
new file mode 100644
index 000000000..b6e6cea9e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Net;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal interface ILdnSocket : IDisposable
+ {
+ bool SendPacketAsync(EndPoint endpoint, byte[] buffer);
+ bool Start();
+ bool Stop();
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
new file mode 100644
index 000000000..97e3bd627
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal interface ILdnTcpSocket : ILdnSocket
+ {
+ bool Connect();
+ void DisconnectAndStop();
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
new file mode 100644
index 000000000..cfe9a8aae
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
@@ -0,0 +1,99 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket
+ {
+ private readonly LanProtocol _protocol;
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+ OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+ OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!");
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size);
+ }
+
+ public void DisconnectAndStop()
+ {
+ DisconnectAsync();
+
+ while (IsConnected)
+ {
+ Thread.Yield();
+ }
+ }
+
+ public bool SendPacketAsync(EndPoint endPoint, byte[] data)
+ {
+ if (endPoint != null)
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null.");
+ }
+
+ if (IsConnecting && !IsConnected)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting...");
+
+ while (IsConnecting && !IsConnected)
+ {
+ Thread.Yield();
+ }
+ }
+
+ return SendAsync(data);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ DisconnectAndStop();
+ base.Dispose(disposingManagedResources);
+ }
+
+ public override bool Connect()
+ {
+ // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues.
+ base.ConnectAsync();
+
+ while (IsConnecting)
+ {
+ Thread.Sleep(1);
+ }
+
+ return IsConnected;
+ }
+
+ public bool Start()
+ {
+ throw new InvalidOperationException("Start was called.");
+ }
+
+ public bool Stop()
+ {
+ throw new InvalidOperationException("Stop was called.");
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
new file mode 100644
index 000000000..0ca12b9f6
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
@@ -0,0 +1,54 @@
+using NetCoreServer;
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket
+ {
+ private readonly LanProtocol _protocol;
+
+ public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ OptionReuseAddress = true;
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}");
+ }
+
+ protected override TcpSession CreateSession()
+ {
+ return new LdnProxyTcpSession(this, _protocol);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ Stop();
+ base.Dispose(disposingManagedResources);
+ }
+
+ public bool Connect()
+ {
+ throw new InvalidOperationException("Connect was called.");
+ }
+
+ public void DisconnectAndStop()
+ {
+ Stop();
+ }
+
+ public bool SendPacketAsync(EndPoint endpoint, byte[] buffer)
+ {
+ throw new InvalidOperationException("SendPacketAsync was called.");
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
new file mode 100644
index 000000000..f30c4b011
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
@@ -0,0 +1,83 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpSession : NetCoreServer.TcpSession
+ {
+ private readonly LanProtocol _protocol;
+
+ internal int NodeId;
+ internal NodeInfo NodeInfo;
+
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server)
+ {
+ _protocol = protocol;
+ _protocol.Connect += OnConnect;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+ OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+ OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+ }
+
+ public void OverrideInfo()
+ {
+ NodeInfo.NodeId = (byte)NodeId;
+ NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0);
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!");
+ }
+
+ protected override void OnDisconnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!");
+
+ _protocol.InvokeDisconnectStation(this);
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}");
+
+ Dispose();
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ _protocol.Connect -= OnConnect;
+ base.Dispose(disposingManagedResources);
+ }
+
+ private void OnConnect(NodeInfo info, EndPoint endPoint)
+ {
+ try
+ {
+ if (endPoint.Equals(this.Socket.RemoteEndPoint))
+ {
+ NodeInfo = info;
+ _protocol.InvokeAccept(this);
+ }
+ }
+ catch (System.ObjectDisposedException)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]");
+
+ _protocol.InvokeDisconnectStation(this);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
new file mode 100644
index 000000000..b1519d1ff
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
@@ -0,0 +1,157 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket
+ {
+ private const long ScanFrequency = 1000;
+
+ private readonly LanProtocol _protocol;
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ private readonly object _scanLock = new();
+
+ private Dictionary _scanResultsLast = new();
+ private Dictionary _scanResults = new();
+ private readonly AutoResetEvent _scanResponse = new(false);
+ private long _lastScanTime;
+
+ public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ _protocol.Scan += HandleScan;
+ _protocol.ScanResponse += HandleScanResponse;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionReuseAddress = true;
+ OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax;
+ OptionSendBufferSize = LanProtocol.TxBufferSizeMax;
+
+ Start();
+ }
+
+ protected override Socket CreateSocket()
+ {
+ return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
+ {
+ EnableBroadcast = true,
+ };
+ }
+
+ protected override void OnStarted()
+ {
+ ReceiveAsync();
+ }
+
+ protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint);
+ ReceiveAsync();
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ _protocol.Scan -= HandleScan;
+ _protocol.ScanResponse -= HandleScanResponse;
+
+ _scanResponse.Dispose();
+
+ base.Dispose(disposingManagedResources);
+ }
+
+ public bool SendPacketAsync(EndPoint endpoint, byte[] data)
+ {
+ return SendAsync(endpoint, data);
+ }
+
+ private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data)
+ {
+ _protocol.SendPacket(this, type, data, endpoint);
+ }
+
+ private void HandleScanResponse(NetworkInfo info)
+ {
+ Span mac = stackalloc byte[8];
+
+ info.Common.MacAddress.AsSpan().CopyTo(mac);
+
+ lock (_scanLock)
+ {
+ _scanResults[BitConverter.ToUInt64(mac)] = info;
+
+ _scanResponse.Set();
+ }
+ }
+
+ public void ClearScanResults()
+ {
+ // Rate limit scans.
+
+ long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000);
+ long delay = ScanFrequency - (timeMs - _lastScanTime);
+
+ if (delay > 0)
+ {
+ Thread.Sleep((int)delay);
+ }
+
+ _lastScanTime = timeMs;
+
+ lock (_scanLock)
+ {
+ var newResults = _scanResultsLast;
+ newResults.Clear();
+
+ _scanResultsLast = _scanResults;
+ _scanResults = newResults;
+
+ _scanResponse.Reset();
+ }
+ }
+
+ public Dictionary GetScanResults()
+ {
+ // NOTE: Try to minimize waiting time for scan results.
+ // After we receive the first response, wait a short time for follow-ups and return.
+ // Responses that were too late to catch will appear in the next scan.
+
+ // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console).
+
+ if (_scanResponse.WaitOne(1000))
+ {
+ // Wait a short while longer in case there are some other responses.
+ Thread.Sleep(33);
+ }
+
+ lock (_scanLock)
+ {
+ var results = new Dictionary();
+
+ foreach (KeyValuePair last in _scanResultsLast)
+ {
+ results[last.Key] = last.Value;
+ }
+
+ foreach (KeyValuePair scan in _scanResults)
+ {
+ results[scan.Key] = scan.Value;
+ }
+
+ return results;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
new file mode 100644
index 000000000..4cebe414d
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 12)]
+ internal struct LanPacketHeader
+ {
+ public uint Magic;
+ public LanPacketType Type;
+ public byte Compressed;
+ public ushort Length;
+ public ushort DecompressLength;
+ public Array2 Reserved;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
new file mode 100644
index 000000000..901f00b00
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+ internal enum LanPacketType : byte
+ {
+ Scan,
+ ScanResponse,
+ Connect,
+ SyncNetwork,
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
similarity index 91%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
index 1cc09c00d..b379d2680 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class NetworkChangeEventArgs : EventArgs
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
index c190d6ed1..e39c01978 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange += NetworkChanged;
}
- private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
index 47e48d0a1..058ce62d0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0xBC)]
struct ConnectPrivateRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
similarity index 84%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
index 9ff46cccb..136589b2a 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
struct ConnectRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
similarity index 88%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
index 6e890618c..ec0668884 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
///
/// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
index 4efe9165a..eecea5eb0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
///
/// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
similarity index 80%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
index 70ebf7e38..cd576e055 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
enum NetworkError : int
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
similarity index 71%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
index acb0b36ac..7e0c2a43f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
struct NetworkErrorMessage
diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs
index 9c6d025eb..b83c642e5 100644
--- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs
@@ -85,10 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl
public void SetSyncpointMinEqualSyncpointMax(uint id)
{
- if (id >= SynchronizationManager.MaxHardwareSyncpoints)
- {
- throw new ArgumentOutOfRangeException(nameof(id));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)SynchronizationManager.MaxHardwareSyncpoints);
int value = (int)ReadSyncpointValue(id);
diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
index 9d7e4d4c5..e892d6ab6 100644
--- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
+++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
@@ -39,6 +39,8 @@ namespace Ryujinx.HLE.HOS.Services
private readonly KernelContext _context;
private KProcess _selfProcess;
private KThread _selfThread;
+ private KEvent _wakeEvent;
+ private int _wakeHandle = 0;
private readonly ReaderWriterLockSlim _handleLock = new();
private readonly Dictionary _sessions = new();
@@ -125,6 +127,8 @@ namespace Ryujinx.HLE.HOS.Services
_handleLock.ExitWriteLock();
}
}
+
+ _wakeEvent.WritableEvent.Signal();
}
private IpcService GetSessionObj(int serverSessionHandle)
@@ -187,6 +191,9 @@ namespace Ryujinx.HLE.HOS.Services
AddPort(serverPortHandle, SmObjectFactory);
}
+ _wakeEvent = new KEvent(_context);
+ Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle);
+
InitDone.Set();
ulong messagePtr = _selfThread.TlsAddress;
@@ -195,7 +202,6 @@ namespace Ryujinx.HLE.HOS.Services
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
_selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
_selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
-
int replyTargetHandle = 0;
while (true)
@@ -211,13 +217,15 @@ namespace Ryujinx.HLE.HOS.Services
portHandleCount = _ports.Count;
- handleCount = portHandleCount + _sessions.Count;
+ handleCount = portHandleCount + _sessions.Count + 1;
handles = ArrayPool.Shared.Rent(handleCount);
- _ports.Keys.CopyTo(handles, 0);
+ handles[0] = _wakeHandle;
- _sessions.Keys.CopyTo(handles, portHandleCount);
+ _ports.Keys.CopyTo(handles, 1);
+
+ _sessions.Keys.CopyTo(handles, portHandleCount + 1);
}
finally
{
@@ -227,8 +235,7 @@ namespace Ryujinx.HLE.HOS.Services
}
}
- // We still need a timeout here to allow the service to pick up and listen new sessions...
- var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L);
+ var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1);
_selfThread.HandlePostSyscall();
@@ -239,7 +246,7 @@ namespace Ryujinx.HLE.HOS.Services
replyTargetHandle = 0;
- if (rc == Result.Success && signaledIndex >= portHandleCount)
+ if (rc == Result.Success && signaledIndex >= portHandleCount + 1)
{
// We got a IPC request, process it, pass to the appropriate service if needed.
int signaledHandle = handles[signaledIndex];
@@ -253,24 +260,32 @@ namespace Ryujinx.HLE.HOS.Services
{
if (rc == Result.Success)
{
- // We got a new connection, accept the session to allow servicing future requests.
- if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
+ if (signaledIndex > 0)
{
- bool handleWriteLockTaken = false;
- try
+ // We got a new connection, accept the session to allow servicing future requests.
+ if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
{
- handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
- IpcService obj = _ports[handles[signaledIndex]].Invoke();
- _sessions.Add(serverSessionHandle, obj);
- }
- finally
- {
- if (handleWriteLockTaken)
+ bool handleWriteLockTaken = false;
+ try
{
- _handleLock.ExitWriteLock();
+ handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
+ IpcService obj = _ports[handles[signaledIndex]].Invoke();
+ _sessions.Add(serverSessionHandle, obj);
+ }
+ finally
+ {
+ if (handleWriteLockTaken)
+ {
+ _handleLock.ExitWriteLock();
+ }
}
}
}
+ else
+ {
+ // The _wakeEvent signalled, which means we have a new session.
+ _wakeEvent.WritableEvent.Clear();
+ }
}
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
@@ -499,6 +514,8 @@ namespace Ryujinx.HLE.HOS.Services
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
{
+ _selfProcess.HandleTable.CloseHandle(_wakeHandle);
+
foreach (IpcService service in _sessions.Values)
{
(service as IDisposable)?.Dispose();
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs
index 07c97182c..62a7ccb59 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs
@@ -27,7 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
int controlLength = message.Control == null ? 0 : message.Control.Length;
BsdSocketFlags flags = message.Flags;
- if (!MemoryMarshal.TryWrite(rawData, ref msgNameLength))
+ if (!MemoryMarshal.TryWrite(rawData, in msgNameLength))
{
return LinuxError.EFAULT;
}
@@ -45,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
rawData = rawData[msgNameLength..];
}
- if (!MemoryMarshal.TryWrite(rawData, ref iovCount))
+ if (!MemoryMarshal.TryWrite(rawData, in iovCount))
{
return LinuxError.EFAULT;
}
@@ -58,7 +58,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
{
ulong iovLength = (ulong)message.Iov[index].Length;
- if (!MemoryMarshal.TryWrite(rawData, ref iovLength))
+ if (!MemoryMarshal.TryWrite(rawData, in iovLength))
{
return LinuxError.EFAULT;
}
@@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
}
}
- if (!MemoryMarshal.TryWrite(rawData, ref controlLength))
+ if (!MemoryMarshal.TryWrite(rawData, in controlLength))
{
return LinuxError.EFAULT;
}
@@ -96,14 +96,14 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
rawData = rawData[controlLength..];
}
- if (!MemoryMarshal.TryWrite(rawData, ref flags))
+ if (!MemoryMarshal.TryWrite(rawData, in flags))
{
return LinuxError.EFAULT;
}
rawData = rawData[sizeof(BsdSocketFlags)..];
- if (!MemoryMarshal.TryWrite(rawData, ref message.Length))
+ if (!MemoryMarshal.TryWrite(rawData, in message.Length))
{
return LinuxError.EFAULT;
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
index d0fb6675a..39af90383 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
@@ -654,7 +654,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres
}
uint sentinel = 0;
- MemoryMarshal.Write(data, ref sentinel);
+ MemoryMarshal.Write(data, in sentinel);
data = data[sizeof(uint)..];
return region.Memory.Span.Length - data.Length;
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs
index a0613d7bc..b57b0d5ca 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs
@@ -94,7 +94,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
Header.ToNetworkOrder();
- MemoryMarshal.Write(buffer, ref Header);
+ MemoryMarshal.Write(buffer, in Header);
buffer = buffer[Unsafe.SizeOf()..];
@@ -103,7 +103,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
AddrInfo4 socketAddress = SocketAddress.Value;
socketAddress.ToNetworkOrder();
- MemoryMarshal.Write(buffer, ref socketAddress);
+ MemoryMarshal.Write(buffer, in socketAddress);
buffer = buffer[Unsafe.SizeOf()..];
}
@@ -117,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
Array4 rawIPv4Address = RawIPv4Address.Value;
AddrInfo4.RawIpv4AddressNetworkEndianSwap(ref rawIPv4Address);
- MemoryMarshal.Write(buffer, ref rawIPv4Address);
+ MemoryMarshal.Write(buffer, in rawIPv4Address);
buffer = buffer[Unsafe.SizeOf>()..];
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
index dab099aab..e3c05df51 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
@@ -161,7 +161,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
}
else
{
- throw exception;
+ throw;
}
}
finally
@@ -206,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
}
else
{
- throw exception;
+ throw;
}
}
finally
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
index d3d9dc030..712d640c2 100644
--- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
+++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
@@ -1,5 +1,7 @@
-using Ryujinx.Common.Configuration;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.PreciseSleep;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap;
@@ -23,9 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
private readonly Thread _composerThread;
- private readonly Stopwatch _chrono;
-
- private readonly ManualResetEvent _event = new(false);
+ private readonly AutoResetEvent _event = new(false);
private readonly AutoResetEvent _nextFrameEvent = new(true);
private long _ticks;
private long _ticksPerFrame;
@@ -64,11 +64,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
_composerThread = new Thread(HandleComposition)
{
Name = "SurfaceFlinger.Composer",
+ Priority = ThreadPriority.AboveNormal
};
- _chrono = new Stopwatch();
- _chrono.Start();
-
_ticks = 0;
_spinTicks = Stopwatch.Frequency / 500;
_1msTicks = Stopwatch.Frequency / 1000;
@@ -299,11 +297,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
_isRunning = true;
- long lastTicks = _chrono.ElapsedTicks;
+ long lastTicks = PerformanceCounter.ElapsedTicks;
while (_isRunning)
{
- long ticks = _chrono.ElapsedTicks;
+ long ticks = PerformanceCounter.ElapsedTicks;
if (_swapInterval == 0)
{
@@ -336,21 +334,16 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
}
// Sleep if possible. If the time til the next frame is too low, spin wait instead.
- long diff = _ticksPerFrame - (_ticks + _chrono.ElapsedTicks - ticks);
+ long diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
if (diff > 0)
{
+ PreciseSleepHelper.SleepUntilTimePoint(_event, PerformanceCounter.ElapsedTicks + diff);
+
+ diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
+
if (diff < _spinTicks)
{
- do
- {
- // SpinWait is a little more HT/SMT friendly than aggressively updating/checking ticks.
- // The value of 5 still gives us quite a bit of precision (~0.0003ms variance at worst) while waiting a reasonable amount of time.
- Thread.SpinWait(5);
-
- ticks = _chrono.ElapsedTicks;
- _ticks += ticks - lastTicks;
- lastTicks = ticks;
- } while (_ticks < _ticksPerFrame);
+ PreciseSleepHelper.SpinWaitUntilTimePoint(PerformanceCounter.ElapsedTicks + diff);
}
else
{
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index 6de99131e..50f7d5853 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -20,7 +20,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage)
+ internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage)
+ where TMetaData : PartitionFileSystemMetaCore, new()
+ where TFormat : IPartitionFileSystemFormat
+ where THeader : unmanaged, IPartitionFileSystemHeader
+ where TEntry : unmanaged, IPartitionFileSystemEntry
{
errorMessage = null;
@@ -91,7 +95,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath))
{
- PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage());
+ PartitionFileSystem updatePartitionFileSystem = new();
+ updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index 51cbb6f99..220b868db 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -69,7 +69,8 @@ namespace Ryujinx.HLE.Loaders.Processes
public bool LoadNsp(string path)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
- PartitionFileSystem partitionFileSystem = new(file.AsStorage());
+ PartitionFileSystem partitionFileSystem = new();
+ partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index 292a5c122..c229b1742 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
@@ -1,8 +1,8 @@
using LibHac.Account;
using LibHac.Common;
using LibHac.Fs;
+using LibHac.Fs.Fsa;
using LibHac.Fs.Shim;
-using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Ns;
@@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes
// TODO: Remove this workaround when ASLR is implemented.
private const ulong CodeStartOffset = 0x500000UL;
- public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem)
+ public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem)
{
ulong applicationId = 0;
int programCount = 0;
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index 5e3aa0eac..370933ccf 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
@@ -27,6 +27,7 @@
+
diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
index d2585c563..7b13df736 100644
--- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
+++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
@@ -1,8 +1,8 @@
- net7.0
- win10-x64;osx-x64;linux-x64
+ net8.0
+ win-x64;osx-x64;linux-x64
Exe
true
1.0.0-dirty
@@ -34,7 +34,7 @@
-
+
@@ -69,4 +69,4 @@
true
partial
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj
index d04c5a9b6..fa1544c4f 100644
--- a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj
+++ b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
index 0139c367f..ae40f7b5e 100644
--- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
+++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
index 817a96e2e..1ab79d08a 100644
--- a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
+++ b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Input/Ryujinx.Input.csproj b/src/Ryujinx.Input/Ryujinx.Input.csproj
index df462734f..59a9eeb61 100644
--- a/src/Ryujinx.Input/Ryujinx.Input.csproj
+++ b/src/Ryujinx.Input/Ryujinx.Input.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs
index 65b4d48f2..b8d48365c 100644
--- a/src/Ryujinx.Memory/AddressSpaceManager.cs
+++ b/src/Ryujinx.Memory/AddressSpaceManager.cs
@@ -455,6 +455,11 @@ namespace Ryujinx.Memory
return _pageTable.Read(va) + (nuint)(va & PageMask);
}
+ ///
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ }
+
///
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{
diff --git a/src/Ryujinx.Memory/IVirtualMemoryManager.cs b/src/Ryujinx.Memory/IVirtualMemoryManager.cs
index edbfc8855..8c9ca1684 100644
--- a/src/Ryujinx.Memory/IVirtualMemoryManager.cs
+++ b/src/Ryujinx.Memory/IVirtualMemoryManager.cs
@@ -104,6 +104,12 @@ namespace Ryujinx.Memory
/// True if the data was changed, false otherwise
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data);
+ ///
+ /// Fills the specified memory region with the value specified in .
+ ///
+ /// Virtual address to fill the value into
+ /// Size of the memory region to fill
+ /// Value to fill with
void Fill(ulong va, ulong size, byte value)
{
const int MaxChunkSize = 1 << 24;
@@ -194,6 +200,14 @@ namespace Ryujinx.Memory
/// Optional ID of the handles that should not be signalled
void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null);
+ ///
+ /// Reprotect a region of virtual memory for guest access.
+ ///
+ /// Virtual address base
+ /// Size of the region to protect
+ /// Memory protection to set
+ void Reprotect(ulong va, ulong size, MemoryPermission protection);
+
///
/// Reprotect a region of virtual memory for tracking.
///
diff --git a/src/Ryujinx.Memory/Range/MultiRange.cs b/src/Ryujinx.Memory/Range/MultiRange.cs
index 7011e528e..5a0b4178a 100644
--- a/src/Ryujinx.Memory/Range/MultiRange.cs
+++ b/src/Ryujinx.Memory/Range/MultiRange.cs
@@ -52,10 +52,7 @@ namespace Ryujinx.Memory.Range
{
if (HasSingleRange)
{
- if (_singleRange.Size - offset < size)
- {
- throw new ArgumentOutOfRangeException(nameof(size));
- }
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(size, _singleRange.Size - offset);
return new MultiRange(_singleRange.Address + offset, size);
}
@@ -108,10 +105,7 @@ namespace Ryujinx.Memory.Range
{
if (HasSingleRange)
{
- if (index != 0)
- {
- throw new ArgumentOutOfRangeException(nameof(index));
- }
+ ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0);
return _singleRange;
}
diff --git a/src/Ryujinx.Memory/Ryujinx.Memory.csproj b/src/Ryujinx.Memory/Ryujinx.Memory.csproj
index 91e46e48e..8310a3e5c 100644
--- a/src/Ryujinx.Memory/Ryujinx.Memory.csproj
+++ b/src/Ryujinx.Memory/Ryujinx.Memory.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
diff --git a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
index 955e6d3f1..8e7953045 100644
--- a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
+++ b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
diff --git a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
index 74b4ec2f7..ab89fb5c7 100644
--- a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
+++ b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
Exe
Debug;Release
diff --git a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs
index 59dc1a525..435bb35ae 100644
--- a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs
+++ b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs
@@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory
throw new NotImplementedException();
}
+ public void Reprotect(ulong va, ulong size, MemoryPermission protection)
+ {
+ throw new NotImplementedException();
+ }
+
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{
OnProtect?.Invoke(va, size, protection);
diff --git a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj
index 4dcb69623..f05060838 100644
--- a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj
+++ b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
false
diff --git a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
index d925546fe..befacfb22 100644
--- a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
+++ b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
Debug;Release
diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj
index ab331ce58..3be9787a3 100644
--- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj
+++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
Exe
false
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
index e6130bdac..65ab01eeb 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
@@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.Ui.Common.Helper;
using System;
-using System.Globalization;
using System.IO;
-using System.Text.Json.Serialization;
namespace Ryujinx.Ui.App.Common
{
@@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common
public string TitleId { get; set; }
public string Developer { get; set; }
public string Version { get; set; }
- public string TimePlayed { get; set; }
- public double TimePlayedNum { get; set; }
+ public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
- public string FileSize { get; set; }
- public double FileSizeBytes { get; set; }
+ public long FileSize { get; set; }
public string Path { get; set; }
public BlitStruct ControlHolder { get; set; }
- [JsonIgnore]
- public string LastPlayedString
- {
- get
- {
- if (!LastPlayed.HasValue)
- {
- // TODO: maybe put localized string here instead of just "Never"
- return "Never";
- }
+ public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
- return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
- }
- }
+ public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
+
+ public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
{
@@ -65,7 +53,7 @@ namespace Ryujinx.Ui.App.Common
if (extension is ".nsp" or ".xci")
{
- PartitionFileSystem pfs;
+ IFileSystem pfs;
if (extension == ".xci")
{
@@ -75,7 +63,9 @@ namespace Ryujinx.Ui.App.Common
}
else
{
- pfs = new PartitionFileSystem(file.AsStorage());
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
}
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 33e6c4aad..46f29851c 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common
return;
}
- double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
+ long fileSize = new FileInfo(applicationPath).Length;
string titleName = "Unknown";
string titleId = "0000000000000000";
string developer = "Unknown";
@@ -174,7 +174,7 @@ namespace Ryujinx.Ui.App.Common
{
try
{
- PartitionFileSystem pfs;
+ IFileSystem pfs;
bool isExeFs = false;
@@ -186,7 +186,9 @@ namespace Ryujinx.Ui.App.Common
}
else
{
- pfs = new PartitionFileSystem(file.AsStorage());
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
bool hasMainNca = false;
@@ -423,25 +425,25 @@ namespace Ryujinx.Ui.App.Common
{
appMetadata.Title = titleName;
- if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
+ // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
+ if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
{
- // Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
- return;
+ appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
+ appMetadata.TimePlayedOld = default;
}
- // Migrate from string-based last_played to DateTime-based last_played_utc.
- if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
+ if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
{
- Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc");
- appMetadata.LastPlayed = lastPlayedOldParsed;
+ // Migrate from string-based last_played to DateTime-based last_played_utc.
+ if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ {
+ appMetadata.LastPlayed = lastPlayedOldParsed;
+
+ // Migration successful: deleting last_played from the metadata file.
+ appMetadata.LastPlayedOld = default;
+ }
- // Migration successful: deleting last_played from the metadata file.
- appMetadata.LastPlayedOld = default;
- }
- else
- {
- // Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it.
- Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)");
}
});
@@ -453,12 +455,10 @@ namespace Ryujinx.Ui.App.Common
TitleId = titleId,
Developer = developer,
Version = version,
- TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
- TimePlayedNum = appMetadata.TimePlayed,
+ TimePlayed = appMetadata.TimePlayed,
LastPlayed = appMetadata.LastPlayed,
- FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
- FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
- FileSizeBytes = fileSize,
+ FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
+ FileSize = fileSize,
Path = applicationPath,
ControlHolder = controlHolder,
};
@@ -500,7 +500,7 @@ namespace Ryujinx.Ui.App.Common
ApplicationCountUpdated?.Invoke(null, e);
}
- private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
+ private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId)
{
(_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
@@ -546,7 +546,7 @@ namespace Ryujinx.Ui.App.Common
return appMetadata;
}
- public byte[] GetApplicationIcon(string applicationPath)
+ public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage)
{
byte[] applicationIcon = null;
@@ -563,7 +563,7 @@ namespace Ryujinx.Ui.App.Common
{
try
{
- PartitionFileSystem pfs;
+ IFileSystem pfs;
bool isExeFs = false;
@@ -575,7 +575,9 @@ namespace Ryujinx.Ui.App.Common
}
else
{
- pfs = new PartitionFileSystem(file.AsStorage());
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{
@@ -600,7 +602,7 @@ namespace Ryujinx.Ui.App.Common
{
using var icon = new UniqueRef();
- controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
@@ -712,31 +714,6 @@ namespace Ryujinx.Ui.App.Common
return applicationIcon ?? _ncaIcon;
}
- private static string ConvertSecondsToFormattedString(double seconds)
- {
- TimeSpan time = TimeSpan.FromSeconds(seconds);
-
- string timeString;
- if (time.Days != 0)
- {
- timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
- }
- else if (time.Hours != 0)
- {
- timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
- }
- else if (time.Minutes != 0)
- {
- timeString = $"{time.Minutes:D2}m";
- }
- else
- {
- timeString = "Never";
- }
-
- return timeString;
- }
-
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
{
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
@@ -827,7 +804,7 @@ namespace Ryujinx.Ui.App.Common
return false;
}
- public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
+ public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex)
{
Nca mainNca = null;
Nca patchNca = null;
@@ -931,7 +908,8 @@ namespace Ryujinx.Ui.App.Common
if (File.Exists(updatePath))
{
FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
- PartitionFileSystem nsp = new(file.AsStorage());
+ PartitionFileSystem nsp = new();
+ nsp.Initialize(file.AsStorage()).ThrowIfFailure();
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
}
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
index 01b857a62..9e2ca6870 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
@@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
{
public string Title { get; set; }
public bool Favorite { get; set; }
- public double TimePlayed { get; set; }
+
+ [JsonPropertyName("timespan_played")]
+ public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
[JsonPropertyName("last_played_utc")]
public DateTime? LastPlayed { get; set; } = null;
+ [JsonPropertyName("time_played")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public double TimePlayedOld { get; set; }
+
[JsonPropertyName("last_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string LastPlayedOld { get; set; }
+
+ ///
+ /// Updates . Call this before launching a game.
+ ///
+ public void UpdatePreGame()
+ {
+ LastPlayed = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates and . Call this after a game ends.
+ ///
+ public void UpdatePostGame()
+ {
+ DateTime? prevLastPlayed = LastPlayed;
+ UpdatePreGame();
+
+ if (!prevLastPlayed.HasValue)
+ {
+ return;
+ }
+
+ TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
+ double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
+ TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
+ }
}
}
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs
deleted file mode 100644
index 71366ba7c..000000000
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Ryujinx.Ui.Common.Configuration
-{
- public enum ConfigurationLoadResult
- {
- Success = 0,
- NotLoaded = 1,
- MigratedFromPreVulkan = 1 << 8,
- }
-}
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index ea6c6af60..79ad7cf8c 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.Vulkan;
using Ryujinx.Ui.Common.Configuration.System;
using Ryujinx.Ui.Common.Configuration.Ui;
using Ryujinx.Ui.Common.Helper;
@@ -578,6 +579,7 @@ namespace Ryujinx.Ui.Common.Configuration
{
LanInterfaceId = new ReactiveObject();
Mode = new ReactiveObject();
+ Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode));
}
}
@@ -770,7 +772,7 @@ namespace Ryujinx.Ui.Common.Configuration
Graphics.ResScaleCustom.Value = 1.0f;
Graphics.MaxAnisotropy.Value = -1.0f;
Graphics.AspectRatio.Value = AspectRatio.Fixed16x9;
- Graphics.GraphicsBackend.Value = OperatingSystem.IsMacOS() ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGl;
+ Graphics.GraphicsBackend.Value = DefaultGraphicsBackend();
Graphics.PreferredGpu.Value = "";
Graphics.ShadersDumpPath.Value = "";
Logger.EnableDebug.Value = false;
@@ -791,7 +793,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true;
ShowConfirmExit.Value = true;
- HideCursor.Value = HideCursorMode.Never;
+ HideCursor.Value = HideCursorMode.OnIdle;
Graphics.EnableVsync.Value = true;
Graphics.EnableShaderCache.Value = true;
Graphics.EnableOGLSpirV.Value = false;
@@ -915,7 +917,7 @@ namespace Ryujinx.Ui.Common.Configuration
};
}
- public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath)
+ public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath)
{
bool configurationFileUpdated = false;
@@ -924,12 +926,8 @@ namespace Ryujinx.Ui.Common.Configuration
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default.");
LoadDefault();
-
- return ConfigurationLoadResult.NotLoaded;
}
- ConfigurationLoadResult result = ConfigurationLoadResult.Success;
-
if (configurationFileFormat.Version < 2)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2.");
@@ -1344,8 +1342,6 @@ namespace Ryujinx.Ui.Common.Configuration
configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl;
- result |= ConfigurationLoadResult.MigratedFromPreVulkan;
-
configurationFileUpdated = true;
}
@@ -1553,8 +1549,18 @@ namespace Ryujinx.Ui.Common.Configuration
Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
}
+ }
- return result;
+ private static GraphicsBackend DefaultGraphicsBackend()
+ {
+ // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan.
+ // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer.
+ if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0)
+ {
+ return GraphicsBackend.Vulkan;
+ }
+
+ return GraphicsBackend.OpenGl;
}
private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName)
diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs
new file mode 100644
index 000000000..103b78c24
--- /dev/null
+++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs
@@ -0,0 +1,167 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using ShellLink;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Runtime.Versioning;
+using Image = System.Drawing.Image;
+
+namespace Ryujinx.Ui.Common.Helper
+{
+ public static class ShortcutHelper
+ {
+ [SupportedOSPlatform("windows")]
+ private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
+ iconPath += ".ico";
+
+ MemoryStream iconDataStream = new(iconData);
+ using Image image = Image.FromStream(iconDataStream);
+ using Bitmap bitmap = new(128, 128);
+ using System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(bitmap);
+ graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphic.DrawImage(image, 0, 0, 128, 128);
+ SaveBitmapAsIcon(bitmap, iconPath);
+
+ var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0);
+ shortcut.StringData.NameString = cleanedAppName;
+ shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
+ }
+
+ [SupportedOSPlatform("linux")]
+ private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
+ var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.desktop");
+ iconPath += ".png";
+
+ var image = SixLabors.ImageSharp.Image.Load(iconData);
+ image.SaveAsPng(iconPath);
+
+ using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
+ outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}");
+ }
+
+ [SupportedOSPlatform("macos")]
+ private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
+ var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist");
+ // Macos .App folder
+ string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents");
+ string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS");
+
+ if (!Directory.Exists(scriptFolderPath))
+ {
+ Directory.CreateDirectory(scriptFolderPath);
+ }
+
+ // Runner script
+ const string ScriptName = "runner.sh";
+ string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
+ using StreamWriter scriptFile = new(scriptPath);
+
+ scriptFile.WriteLine("#!/bin/sh");
+ scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}");
+
+ // Set execute permission
+ FileInfo fileInfo = new(scriptPath);
+ fileInfo.UnixFileMode |= UnixFileMode.UserExecute;
+
+ // img
+ string resourceFolderPath = Path.Combine(contentFolderPath, "Resources");
+ if (!Directory.Exists(resourceFolderPath))
+ {
+ Directory.CreateDirectory(resourceFolderPath);
+ }
+
+ const string IconName = "icon.png";
+ var image = SixLabors.ImageSharp.Image.Load(iconData);
+ image.SaveAsPng(Path.Combine(resourceFolderPath, IconName));
+
+ // plist file
+ using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist"));
+ outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName);
+ }
+
+ public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData)
+ {
+ string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+ string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars()));
+
+ if (OperatingSystem.IsWindows())
+ {
+ string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
+
+ CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
+
+ return;
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
+
+ Directory.CreateDirectory(iconPath);
+ CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
+
+ return;
+ }
+
+ if (OperatingSystem.IsMacOS())
+ {
+ CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
+
+ return;
+ }
+
+ throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
+ }
+
+ private static string GetArgsString(string appFilePath)
+ {
+ // args are first defined as a list, for easier adjustments in the future
+ var argsList = new List();
+
+ if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
+ {
+ argsList.Add("--root-data-dir");
+ argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
+ }
+
+ argsList.Add($"\"{appFilePath}\"");
+
+ return String.Join(" ", argsList);
+ }
+
+ ///
+ /// Creates a Icon (.ico) file using the source bitmap image at the specified file path.
+ ///
+ /// The source bitmap image that will be saved as an .ico file
+ /// The location that the new .ico file will be saved too (Make sure to include '.ico' in the path).
+ [SupportedOSPlatform("windows")]
+ private static void SaveBitmapAsIcon(Bitmap source, string filePath)
+ {
+ // Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz
+ byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 };
+ using FileStream fs = new(filePath, FileMode.Create);
+
+ fs.Write(header);
+ // Writing actual data
+ source.Save(fs, ImageFormat.Png);
+ // Getting data length (file length minus header)
+ long dataLength = fs.Length - header.Length;
+ // Write it in the correct place
+ fs.Seek(14, SeekOrigin.Begin);
+ fs.WriteByte((byte)dataLength);
+ fs.WriteByte((byte)(dataLength >> 8));
+ }
+ }
+}
diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
new file mode 100644
index 000000000..951cd089e
--- /dev/null
+++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace Ryujinx.Ui.Common.Helper
+{
+ public static class ValueFormatUtils
+ {
+ private static readonly string[] _fileSizeUnitStrings =
+ {
+ "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing
+ "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values
+ };
+
+ ///
+ /// Used by .
+ ///
+ public enum FileSizeUnits
+ {
+ Auto = -1,
+ Bytes = 0,
+ Kibibytes = 1,
+ Mebibytes = 2,
+ Gibibytes = 3,
+ Tebibytes = 4,
+ Pebibytes = 5,
+ Exbibytes = 6,
+ Kilobytes = 7,
+ Megabytes = 8,
+ Gigabytes = 9,
+ Terabytes = 10,
+ Petabytes = 11,
+ Exabytes = 12,
+ }
+
+ private const double SizeBase10 = 1000;
+ private const double SizeBase2 = 1024;
+ private const int UnitEBIndex = 6;
+
+ #region Value formatters
+
+ ///
+ /// Creates a human-readable string from a .
+ ///
+ /// The to be formatted.
+ /// A formatted string that can be displayed in the UI.
+ public static string FormatTimeSpan(TimeSpan? timeSpan)
+ {
+ if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
+ {
+ // Game was never played
+ return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
+ }
+
+ if (timeSpan.Value.TotalDays < 1)
+ {
+ // Game was played for less than a day
+ return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
+ }
+
+ // Game was played for more than a day
+ TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
+ string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
+
+ return $"{timeSpan.Value.Days}d, {onlyTimeString}";
+ }
+
+ ///
+ /// Creates a human-readable string from a .
+ ///
+ /// The to be formatted. This is expected to be UTC-based.
+ /// The that's used in formatting. Defaults to .
+ /// A formatted string that can be displayed in the UI.
+ public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
+ {
+ culture ??= CultureInfo.CurrentCulture;
+
+ if (!utcDateTime.HasValue)
+ {
+ // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
+ return "Never";
+ }
+
+ return utcDateTime.Value.ToLocalTime().ToString(culture);
+ }
+
+ ///
+ /// Creates a human-readable file size string.
+ ///
+ /// The file size in bytes.
+ /// Formats the passed size value as this unit, bypassing the automatic unit choice.
+ /// A human-readable file size string.
+ public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
+ {
+ if (size <= 0)
+ {
+ return $"0 {_fileSizeUnitStrings[0]}";
+ }
+
+ int unitIndex = (int)forceUnit;
+ if (forceUnit == FileSizeUnits.Auto)
+ {
+ unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
+
+ // Apply an upper bound so that exabytes are the biggest unit used when formatting.
+ if (unitIndex > UnitEBIndex)
+ {
+ unitIndex = UnitEBIndex;
+ }
+ }
+
+ double sizeRounded;
+
+ if (unitIndex > UnitEBIndex)
+ {
+ sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
+ }
+ else
+ {
+ sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
+ }
+
+ string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
+
+ return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
+ }
+
+ #endregion
+
+ #region Value parsers
+
+ ///
+ /// Parses a string generated by and returns the original .
+ ///
+ /// A string representing a .
+ /// A object. If the input string couldn't been parsed, is returned.
+ public static TimeSpan ParseTimeSpan(string timeSpanString)
+ {
+ TimeSpan returnTimeSpan = TimeSpan.Zero;
+
+ // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day.
+ // Here, we split the input string to check if it's the former or the latter.
+ var valueSplit = timeSpanString.Split(", ");
+ if (valueSplit.Length > 1)
+ {
+ var dayPart = valueSplit[0].Split("d")[0];
+ if (int.TryParse(dayPart, out int days))
+ {
+ returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
+ }
+ }
+
+ if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
+ {
+ returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
+ }
+
+ return returnTimeSpan;
+ }
+
+ ///
+ /// Parses a string generated by and returns the original .
+ ///
+ /// The string representing a .
+ /// A object. If the input string couldn't be parsed, is returned.
+ public static DateTime ParseDateTime(string dateTimeString)
+ {
+ if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
+ {
+ // Games that were never played are supposed to appear before the oldest played games in the list,
+ // so returning DateTime.UnixEpoch here makes sense.
+ return DateTime.UnixEpoch;
+ }
+
+ return parsedDateTime;
+ }
+
+ ///
+ /// Parses a string generated by and returns a representing a number of bytes.
+ ///
+ /// A string representing a file size formatted with .
+ /// A representing a number of bytes.
+ public static long ParseFileSize(string sizeString)
+ {
+ // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
+ for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
+ {
+ string unit = _fileSizeUnitStrings[i];
+ if (!sizeString.EndsWith(unit))
+ {
+ continue;
+ }
+
+ string numberString = sizeString.Split(" ")[0];
+ if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
+ {
+ break;
+ }
+
+ double sizeBase = SizeBase2;
+
+ // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value.
+ if (i > UnitEBIndex)
+ {
+ i -= UnitEBIndex;
+ sizeBase = SizeBase10;
+ }
+
+ number *= Math.Pow(sizeBase, i);
+
+ return Convert.ToInt64(number);
+ }
+
+ return 0;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
index 511a03897..74331fdef 100644
--- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
+++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
true
@@ -45,13 +45,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
similarity index 98%
rename from src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
index 08aa452eb..5f1ab5416 100644
--- a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
@@ -5,7 +5,7 @@ using System.Globalization;
using System.IO;
using System.Runtime.Versioning;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("linux")]
class LinuxSystemInfo : SystemInfo
diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
similarity index 94%
rename from src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
index 98a0d8abf..3508ae3a4 100644
--- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
@@ -5,13 +5,20 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("macos")]
partial class MacOSSystemInfo : SystemInfo
{
internal MacOSSystemInfo()
{
+ if (SysctlByName("kern.osversion", out string buildRevision) != 0)
+ {
+ buildRevision = "Unknown Build";
+ }
+
+ OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})";
+
string cpuName = GetCpuidCpuName();
if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0)
diff --git a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
similarity index 87%
rename from src/Ryujinx.Common/SystemInfo/SystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
index 55ec0127c..6a4fe6803 100644
--- a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
@@ -1,10 +1,11 @@
using Ryujinx.Common.Logging;
+using Ryujinx.Ui.Common.Helper;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using System.Text;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
public class SystemInfo
{
@@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo
CpuName = "Unknown";
}
- private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
+ private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes);
public void Print()
{
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
- Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
+ Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}");
}
public static SystemInfo Gather()
diff --git a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
similarity index 98%
rename from src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
index 3b36d6e2e..9bb0fbf74 100644
--- a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
@@ -4,7 +4,7 @@ using System.Management;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("windows")]
partial class WindowsSystemInfo : SystemInfo
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 50151d733..597d00f30 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -3,7 +3,6 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
@@ -11,6 +10,7 @@ using Ryujinx.Ui;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Common.SystemInfo;
using Ryujinx.Ui.Widgets;
using SixLabors.ImageSharp.Formats.Jpeg;
using System;
@@ -177,8 +177,6 @@ namespace Ryujinx
? appDataConfigurationPath
: null;
- bool showVulkanPrompt = false;
-
if (ConfigurationPath == null)
{
// No configuration, we load the default values and save it to disk
@@ -186,26 +184,17 @@ namespace Ryujinx
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
-
- showVulkanPrompt = true;
}
else
{
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
{
- ConfigurationLoadResult result = ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
-
- if ((result & ConfigurationLoadResult.MigratedFromPreVulkan) != 0)
- {
- showVulkanPrompt = true;
- }
+ ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
}
else
{
ConfigurationState.Instance.LoadDefault();
- showVulkanPrompt = true;
-
Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
}
}
@@ -216,12 +205,10 @@ namespace Ryujinx
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
- showVulkanPrompt = false;
}
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
- showVulkanPrompt = false;
}
}
@@ -343,35 +330,6 @@ namespace Ryujinx
}, TaskContinuationOptions.OnlyOnFaulted);
}
- if (showVulkanPrompt)
- {
- var buttonTexts = new Dictionary()
- {
- { 0, "Yes (Vulkan)" },
- { 1, "No (OpenGL)" },
- };
-
- ResponseType response = GtkDialog.CreateCustomDialog(
- "Ryujinx - Default graphics backend",
- "Use Vulkan as default graphics backend?",
- "Ryujinx now supports the Vulkan API. " +
- "Vulkan greatly improves shader compilation performance, " +
- "and fixes some graphical glitches; however, since it is a new feature, " +
- "you may experience some issues that did not occur with OpenGL.\n\n" +
- "Note that you will also lose any existing shader cache the first time you start a game " +
- "on version 1.1.200 onwards, because Vulkan required changes to the shader cache that makes it incompatible with previous versions.\n\n" +
- "Would you like to set Vulkan as the default graphics backend? " +
- "You can change this at any time on the settings window.",
- buttonTexts,
- MessageType.Question);
-
- ConfigurationState.Instance.Graphics.GraphicsBackend.Value = response == 0
- ? GraphicsBackend.Vulkan
- : GraphicsBackend.OpenGl;
-
- ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
- }
-
Application.Run();
}
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index cf4435e57..9890b761b 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -1,8 +1,8 @@
- net7.0
- win10-x64;osx-x64;linux-x64
+ net8.0
+ win-x64;osx-x64;linux-x64
Exe
true
1.0.0-dirty
@@ -25,7 +25,7 @@
-
+
@@ -63,15 +63,15 @@
-
-
+
+
Always
-
-
- Always
- mime\Ryujinx.xml
-
-
+
+
+ Always
+ mime\Ryujinx.xml
+
+
@@ -101,4 +101,4 @@
-
\ No newline at end of file
+
diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs
index 0c0eefd2c..c7a72ab9b 100644
--- a/src/Ryujinx/Ui/Helper/SortHelper.cs
+++ b/src/Ryujinx/Ui/Helper/SortHelper.cs
@@ -1,4 +1,5 @@
using Gtk;
+using Ryujinx.Ui.Common.Helper;
using System;
namespace Ryujinx.Ui.Helper
@@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper
{
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
- static string ReverseFormat(string time)
- {
- if (time == "Never")
- {
- return "00";
- }
+ TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
+ TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
- var numbers = time.Split(new char[] { 'd', 'h', 'm' });
-
- time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
-
- if (numbers.Length == 2)
- {
- return $"00.00:{time}";
- }
- else if (numbers.Length == 3)
- {
- return $"00.{time}";
- }
-
- return time;
- }
-
- string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
- string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
-
- return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
+ return TimeSpan.Compare(aTimeSpan, bTimeSpan);
}
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
- string aValue = model.GetValue(a, 6).ToString();
- string bValue = model.GetValue(b, 6).ToString();
+ DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
+ DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
- if (aValue == "Never")
- {
- aValue = DateTime.UnixEpoch.ToString();
- }
-
- if (bValue == "Never")
- {
- bValue = DateTime.UnixEpoch.ToString();
- }
-
- return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
+ return DateTime.Compare(aDateTime, bDateTime);
}
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
{
- string aValue = model.GetValue(a, 8).ToString();
- string bValue = model.GetValue(b, 8).ToString();
+ long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
+ long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
- if (aValue[^3..] == "GiB")
- {
- aValue = (float.Parse(aValue[0..^3]) * 1024).ToString();
- }
- else
- {
- aValue = aValue[0..^3];
- }
-
- if (bValue[^3..] == "GiB")
- {
- bValue = (float.Parse(bValue[0..^3]) * 1024).ToString();
- }
- else
- {
- bValue = bValue[0..^3];
- }
-
- if (float.Parse(aValue) > float.Parse(bValue))
- {
- return -1;
- }
- else if (float.Parse(bValue) > float.Parse(aValue))
- {
- return 1;
- }
- else
- {
- return 0;
- }
+ return aSize.CompareTo(bSize);
}
}
}
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
index 0d6cab67b..3d02690a7 100644
--- a/src/Ryujinx/Ui/MainWindow.cs
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -954,7 +954,7 @@ namespace Ryujinx.Ui
ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePreGame();
});
}
}
@@ -1097,13 +1097,7 @@ namespace Ryujinx.Ui
{
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{
- if (appMetadata.LastPlayed.HasValue)
- {
- double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
- appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
- }
-
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePostGame();
});
}
}
@@ -1122,6 +1116,14 @@ namespace Ryujinx.Ui
Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
}
+ public void UpdateInternetAccess()
+ {
+ if (_gameLoaded)
+ {
+ _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value;
+ }
+ }
+
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
@@ -1170,10 +1172,10 @@ namespace Ryujinx.Ui
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
- args.AppData.TimePlayed,
+ args.AppData.TimePlayedString,
args.AppData.LastPlayedString,
args.AppData.FileExtension,
- args.AppData.FileSize,
+ args.AppData.FileSizeString,
args.AppData.Path,
args.AppData.ControlHolder);
});
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
index 0f7b4f22b..734437eea 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
@@ -23,6 +23,7 @@ namespace Ryujinx.Ui.Widgets
private MenuItem _purgeShaderCacheMenuItem;
private MenuItem _openPtcDirMenuItem;
private MenuItem _openShaderCacheDirMenuItem;
+ private MenuItem _createShortcutMenuItem;
private void InitializeComponent()
{
@@ -187,6 +188,15 @@ namespace Ryujinx.Ui.Widgets
};
_openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked;
+ //
+ // _createShortcutMenuItem
+ //
+ _createShortcutMenuItem = new MenuItem("Create Application Shortcut")
+ {
+ TooltipText = "Create a Desktop Shortcut that launches the selected Application."
+ };
+ _createShortcutMenuItem.Activated += CreateShortcut_Clicked;
+
ShowComponent();
}
@@ -201,6 +211,8 @@ namespace Ryujinx.Ui.Widgets
_manageSubMenu.Append(_openPtcDirMenuItem);
_manageSubMenu.Append(_openShaderCacheDirMenuItem);
+ Add(_createShortcutMenuItem);
+ Add(new SeparatorMenuItem());
Add(_openSaveUserDirMenuItem);
Add(_openSaveDeviceDirMenuItem);
Add(_openSaveBcatDirMenuItem);
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
index c2e0d8ebc..5af181b08 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -10,6 +10,7 @@ using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
@@ -77,6 +78,8 @@ namespace Ryujinx.Ui.Widgets
_extractExeFsMenuItem.Sensitive = hasNca;
_extractLogoMenuItem.Sensitive = hasNca;
+ _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild();
+
PopupAtPointer(null);
}
@@ -208,7 +211,7 @@ namespace Ryujinx.Ui.Widgets
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
{
- PartitionFileSystem pfs;
+ IFileSystem pfs;
if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
{
@@ -218,7 +221,9 @@ namespace Ryujinx.Ui.Widgets
}
else
{
- pfs = new PartitionFileSystem(file.AsStorage());
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
}
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
@@ -629,5 +634,11 @@ namespace Ryujinx.Ui.Widgets
}
}
}
+
+ private void CreateShortcut_Clicked(object sender, EventArgs args)
+ {
+ byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
+ ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
+ }
}
}
diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs
index 74aef00f4..9f7179467 100644
--- a/src/Ryujinx/Ui/Windows/DlcWindow.cs
+++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs
@@ -88,7 +88,8 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
- PartitionFileSystem pfs = new(containerFile.AsStorage());
+ PartitionFileSystem pfs = new();
+ pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(pfs);
@@ -153,7 +154,8 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(containerPath);
- PartitionFileSystem pfs = new(containerFile.AsStorage());
+ PartitionFileSystem pfs = new();
+ pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs);
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
index ecd712e8e..283155ff7 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -678,6 +678,8 @@ namespace Ryujinx.Ui.Windows
}
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+
+ _parent.UpdateInternetAccess();
MainWindow.UpdateGraphicsConfig();
ThemeHelper.ApplyTheme();
}
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
index c3c2b6aa4..4b60fa793 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -3011,6 +3011,7 @@
Disabled
- Disabled
+ - ldn_mitm
@@ -3082,7 +3083,7 @@
@@ -3097,7 +3098,7 @@