Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
617c5700ca | |||
7b62f7475e | |||
841dd56f4c | |||
a16d582a10 | |||
9ef0be477b | |||
c14ce4d2a5 | |||
171b46ef49 | |||
56fe2ff535 | |||
b1f8f868f6 | |||
d773d5152e | |||
33ba170315 | |||
638be5f296 | |||
49b37550ca | |||
a42f0bbb87 | |||
b4bb22ba06 | |||
6fdf774845 | |||
76b53e018a | |||
28dd7d80af | |||
1e06b28b22 | |||
e768a54f17 | |||
4e2bb13080 | |||
ac4f2c1e70 | |||
e40470bbe1 | |||
f460ecc182 | |||
086564c3c8 | |||
b6ac45d36d | |||
7afae8c699 | |||
7835968214 | |||
0aceb534cb | |||
a0af6e4d07 |
@ -3,13 +3,13 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.0.4" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.4" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.4" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.4" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.4" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.2" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.2" />
|
||||
<PackageVersion Include="Avalonia" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.5" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.3" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.3" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="Concentus" Version="1.1.7" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
@ -18,12 +18,13 @@
|
||||
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
|
||||
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
||||
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
|
||||
<PackageVersion Include="LibHac" Version="0.18.0" />
|
||||
<PackageVersion Include="LibHac" Version="0.19.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||
<PackageVersion Include="NetCoreServer" Version="7.0.0" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageVersion Include="OpenTK.Core" Version="4.7.7" />
|
||||
@ -35,6 +36,7 @@
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.GtkSharp" Version="3.24.24.59-ryujinx" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.28.1-build28" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
|
||||
|
@ -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.
|
||||
|
@ -681,4 +681,33 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
```
|
||||
</details>
|
||||
</details>
|
||||
|
||||
# ShellLink (MIT)
|
||||
<details>
|
||||
<summary>See License</summary>
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
</details>
|
||||
|
@ -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;
|
||||
|
13
distribution/linux/shortcut-template.desktop
Normal file
13
distribution/linux/shortcut-template.desktop
Normal file
@ -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
|
35
distribution/macos/shortcut-template.plist
Normal file
35
distribution/macos/shortcut-template.plist
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{0}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{1}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>{2}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2018 - 2023 Ryujinx Team and Contributors.</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>11.0</string>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
<key>LSEnvironment</key>
|
||||
<dict>
|
||||
<key>DOTNET_DefaultStackSize</key>
|
||||
<string>200000</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -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;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace ARMeilleure.Diagnostics
|
||||
{
|
||||
@ -33,7 +34,6 @@ namespace ARMeilleure.Diagnostics
|
||||
|
||||
public static string Get(ulong address)
|
||||
{
|
||||
|
||||
if (_symbols.TryGetValue(address, out string result))
|
||||
{
|
||||
return result;
|
||||
@ -48,13 +48,15 @@ namespace ARMeilleure.Diagnostics
|
||||
ulong diff = address - symbol.Start;
|
||||
ulong rem = diff % symbol.ElementSize;
|
||||
|
||||
result = symbol.Name + "_" + diff / symbol.ElementSize;
|
||||
StringBuilder resultBuilder = new();
|
||||
resultBuilder.Append($"{symbol.Name}_{diff / symbol.ElementSize}");
|
||||
|
||||
if (rem != 0)
|
||||
{
|
||||
result += "+" + rem;
|
||||
resultBuilder.Append($"+{rem}");
|
||||
}
|
||||
|
||||
result = resultBuilder.ToString();
|
||||
_symbols.TryAdd(address, result);
|
||||
|
||||
return result;
|
||||
|
@ -7,14 +7,14 @@ namespace ARMeilleure.Translation
|
||||
internal class TranslatorCache<T>
|
||||
{
|
||||
private readonly IntervalTree<ulong, T> _tree;
|
||||
private readonly ReaderWriterLock _treeLock;
|
||||
private readonly ReaderWriterLockSlim _treeLock;
|
||||
|
||||
public int Count => _tree.Count;
|
||||
|
||||
public TranslatorCache()
|
||||
{
|
||||
_tree = new IntervalTree<ulong, T>();
|
||||
_treeLock = new ReaderWriterLock();
|
||||
_treeLock = new ReaderWriterLockSlim();
|
||||
}
|
||||
|
||||
public bool TryAdd(ulong address, ulong size, T value)
|
||||
@ -24,70 +24,70 @@ namespace ARMeilleure.Translation
|
||||
|
||||
public bool AddOrUpdate(ulong address, ulong size, T value, Func<ulong, T, T> updateFactoryCallback)
|
||||
{
|
||||
_treeLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_treeLock.EnterWriteLock();
|
||||
bool result = _tree.AddOrUpdate(address, address + size, value, updateFactoryCallback);
|
||||
_treeLock.ReleaseWriterLock();
|
||||
_treeLock.ExitWriteLock();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public T GetOrAdd(ulong address, ulong size, T value)
|
||||
{
|
||||
_treeLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_treeLock.EnterWriteLock();
|
||||
value = _tree.GetOrAdd(address, address + size, value);
|
||||
_treeLock.ReleaseWriterLock();
|
||||
_treeLock.ExitWriteLock();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool Remove(ulong address)
|
||||
{
|
||||
_treeLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_treeLock.EnterWriteLock();
|
||||
bool removed = _tree.Remove(address) != 0;
|
||||
_treeLock.ReleaseWriterLock();
|
||||
_treeLock.ExitWriteLock();
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_treeLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_treeLock.EnterWriteLock();
|
||||
_tree.Clear();
|
||||
_treeLock.ReleaseWriterLock();
|
||||
_treeLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
public bool ContainsKey(ulong address)
|
||||
{
|
||||
_treeLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_treeLock.EnterReadLock();
|
||||
bool result = _tree.ContainsKey(address);
|
||||
_treeLock.ReleaseReaderLock();
|
||||
_treeLock.ExitReadLock();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryGetValue(ulong address, out T value)
|
||||
{
|
||||
_treeLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_treeLock.EnterReadLock();
|
||||
bool result = _tree.TryGet(address, out value);
|
||||
_treeLock.ReleaseReaderLock();
|
||||
_treeLock.ExitReadLock();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public int GetOverlaps(ulong address, ulong size, ref ulong[] overlaps)
|
||||
{
|
||||
_treeLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_treeLock.EnterReadLock();
|
||||
int count = _tree.Get(address, address + size, ref overlaps);
|
||||
_treeLock.ReleaseReaderLock();
|
||||
_treeLock.ExitReadLock();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public List<T> AsList()
|
||||
{
|
||||
_treeLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_treeLock.EnterReadLock();
|
||||
List<T> list = _tree.AsList();
|
||||
_treeLock.ReleaseReaderLock();
|
||||
_treeLock.ExitReadLock();
|
||||
|
||||
return list;
|
||||
}
|
||||
|
@ -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<bool> e)
|
||||
{
|
||||
Device.Configuration.EnableInternetAccess = e.NewValue;
|
||||
}
|
||||
|
||||
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
|
||||
{
|
||||
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
|
||||
|
@ -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",
|
||||
@ -648,7 +650,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",
|
||||
|
@ -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"))
|
||||
|
@ -145,4 +145,4 @@
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Assets\Locales\en_US.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
@ -82,4 +82,9 @@
|
||||
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
<MenuItem
|
||||
Click="CreateApplicationShortcut_Click"
|
||||
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
|
||||
</MenuFlyout>
|
||||
|
@ -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;
|
||||
|
31
src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs
Normal file
31
src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -327,7 +327,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image;
|
||||
|
||||
string usageString = "";
|
||||
StringBuilder usageStringBuilder = new();
|
||||
|
||||
for (int i = 0; i < _amiiboList.Count; i++)
|
||||
{
|
||||
@ -341,20 +341,19 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||
{
|
||||
usageString += Environment.NewLine +
|
||||
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
|
||||
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
|
||||
|
||||
writable = usageItem.Write;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usageString.Length == 0)
|
||||
if (usageStringBuilder.Length == 0)
|
||||
{
|
||||
usageString = LocaleManager.Instance[LocaleKeys.Unknown] + ".";
|
||||
usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}.");
|
||||
}
|
||||
|
||||
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageString}";
|
||||
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageStringBuilder}";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
@ -1278,6 +1280,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Glyph = Glyph.Grid;
|
||||
}
|
||||
|
||||
public void SetAspectRatio(AspectRatio aspectRatio)
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Value = aspectRatio;
|
||||
}
|
||||
|
||||
public async Task InstallFirmwareFromFile()
|
||||
{
|
||||
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
@ -1483,7 +1490,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
Logger.RestartTime();
|
||||
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path);
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
@ -1691,7 +1698,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="130"
|
||||
Maximum="1"
|
||||
TickFrequency="0.01"
|
||||
@ -480,7 +481,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="130"
|
||||
Maximum="2"
|
||||
TickFrequency="0.01"
|
||||
@ -604,7 +605,7 @@
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="130"
|
||||
Maximum="1"
|
||||
TickFrequency="0.01"
|
||||
@ -1083,7 +1084,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="130"
|
||||
Maximum="1"
|
||||
TickFrequency="0.01"
|
||||
@ -1105,7 +1106,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="130"
|
||||
Maximum="2"
|
||||
TickFrequency="0.01"
|
||||
@ -1125,4 +1126,4 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
@ -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}" />
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Margin="0,-5,0,-5"
|
||||
Width="150"
|
||||
MaxWidth="150"
|
||||
TickFrequency="0.01"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
SmallChange="0.01"
|
||||
Maximum="100"
|
||||
@ -45,11 +46,11 @@
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionGyroDeadzone}" />
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Margin="0,-5,0,-5"
|
||||
Width="150"
|
||||
MaxWidth="150"
|
||||
TickFrequency="0.01"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
SmallChange="0.01"
|
||||
Maximum="100"
|
||||
@ -167,4 +168,4 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
@ -21,7 +22,7 @@
|
||||
TextWrapping="WrapWithOverflow"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsRumbleStrongMultiplier}" />
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Margin="0,-5,0,-5"
|
||||
Width="200"
|
||||
TickFrequency="0.01"
|
||||
@ -41,7 +42,7 @@
|
||||
TextWrapping="WrapWithOverflow"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsRumbleWeakMultiplier}" />
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Margin="0,-5,0,-5"
|
||||
Width="200"
|
||||
MaxWidth="200"
|
||||
@ -58,4 +59,4 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
@ -3,9 +3,11 @@
|
||||
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"
|
||||
xmlns:config="clr-namespace:Ryujinx.Common.Configuration;assembly=Ryujinx.Common"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
|
||||
x:DataType="viewModels:MainWindowViewModel">
|
||||
@ -112,15 +114,52 @@
|
||||
Background="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
<SplitButton
|
||||
Name="AspectRatioStatus"
|
||||
Margin="5,0,5,0"
|
||||
Padding="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="AspectRatioStatus_PointerReleased"
|
||||
Text="{Binding AspectRatioStatusText}"
|
||||
TextAlignment="Left" />
|
||||
Content="{Binding AspectRatioStatusText}"
|
||||
Click="AspectRatioStatus_OnClick"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
|
||||
<SplitButton.Styles>
|
||||
<Style Selector="Border#SeparatorBorder">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</SplitButton.Styles>
|
||||
<SplitButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatio4x3}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Fixed4x3}"/>
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatio16x9}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Fixed16x9}"/>
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatio16x10}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Fixed16x10}"/>
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatio21x9}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Fixed21x9}"/>
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatio32x9}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Fixed32x9}"/>
|
||||
<MenuItem
|
||||
Header="{locale:Locale SettingsTabGraphicsAspectRatioStretch}"
|
||||
Command="{Binding SetAspectRatio}"
|
||||
CommandParameter="{x:Static config:AspectRatio.Stretched}"/>
|
||||
</MenuFlyout>
|
||||
</SplitButton.Flyout>
|
||||
</SplitButton>
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
@ -138,6 +177,7 @@
|
||||
Content="{Binding VolumeStatusText}"
|
||||
IsChecked="{Binding VolumeMuted}"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerWheelChanged="VolumeStatus_OnPointerWheelChanged"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0">
|
||||
@ -154,7 +194,7 @@
|
||||
<ToggleSplitButton.Flyout>
|
||||
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
|
||||
<Grid Margin="0">
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
MaxHeight="40"
|
||||
Width="150"
|
||||
Margin="0"
|
||||
|
@ -43,10 +43,9 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
}
|
||||
|
||||
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
private void AspectRatioStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
|
||||
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
|
||||
}
|
||||
|
||||
@ -54,5 +53,20 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
Window.LoadApplications();
|
||||
}
|
||||
|
||||
private void VolumeStatus_OnPointerWheelChanged(object sender, PointerWheelEventArgs e)
|
||||
{
|
||||
// Change the volume by 5% at a time
|
||||
float newValue = Window.ViewModel.Volume + (float)e.Delta.Y * 0.05f;
|
||||
|
||||
Window.ViewModel.Volume = newValue switch
|
||||
{
|
||||
< 0 => 0,
|
||||
> 1 => 1,
|
||||
_ => newValue,
|
||||
};
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}" />
|
||||
<Slider
|
||||
<controls:SliderScroll
|
||||
Width="150"
|
||||
Height="35"
|
||||
Margin="5,-10,5,0"
|
||||
@ -173,4 +174,4 @@
|
||||
DockPanel.Dock="Right"
|
||||
Text="{locale:Locale CommonSort}" />
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
@ -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" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<Slider Value="{Binding Volume}"
|
||||
<controls:SliderScroll Value="{Binding Volume}"
|
||||
Margin="250,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="5"
|
||||
TickFrequency="5"
|
||||
SmallChange="1"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="10"
|
||||
Width="350" />
|
||||
@ -77,4 +78,4 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
@ -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"
|
||||
@ -173,7 +174,7 @@
|
||||
<TextBlock Text="FSR" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<Slider Value="{Binding ScalingFilterLevel}"
|
||||
<controls:SliderScroll Value="{Binding ScalingFilterLevel}"
|
||||
ToolTip.Tip="{locale:Locale GraphicsScalingFilterLevelTooltip}"
|
||||
MinWidth="150"
|
||||
Margin="10,-3,0,0"
|
||||
|
@ -3,5 +3,6 @@
|
||||
public enum MultiplayerMode
|
||||
{
|
||||
Disabled,
|
||||
LdnMitm,
|
||||
}
|
||||
}
|
||||
|
@ -756,6 +756,18 @@ namespace Ryujinx.Common.Memory
|
||||
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
||||
}
|
||||
|
||||
public struct Array96<T> : IArray<T> where T : unmanaged
|
||||
{
|
||||
T _e0;
|
||||
Array64<T> _other;
|
||||
Array31<T> _other2;
|
||||
public readonly int Length => 96;
|
||||
public ref T this[int index] => ref AsSpan()[index];
|
||||
|
||||
[Pure]
|
||||
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
||||
}
|
||||
|
||||
public struct Array127<T> : IArray<T> where T : unmanaged
|
||||
{
|
||||
T _e0;
|
||||
|
@ -5,7 +5,7 @@ namespace Ryujinx.Common
|
||||
{
|
||||
public class ReactiveObject<T>
|
||||
{
|
||||
private readonly ReaderWriterLock _readerWriterLock = new();
|
||||
private readonly ReaderWriterLockSlim _readerWriterLock = new();
|
||||
private bool _isInitialized;
|
||||
private T _value;
|
||||
|
||||
@ -15,15 +15,15 @@ namespace Ryujinx.Common
|
||||
{
|
||||
get
|
||||
{
|
||||
_readerWriterLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_readerWriterLock.EnterReadLock();
|
||||
T value = _value;
|
||||
_readerWriterLock.ReleaseReaderLock();
|
||||
_readerWriterLock.ExitReadLock();
|
||||
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_readerWriterLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_readerWriterLock.EnterWriteLock();
|
||||
|
||||
T oldValue = _value;
|
||||
|
||||
@ -32,7 +32,7 @@ namespace Ryujinx.Common
|
||||
_isInitialized = true;
|
||||
_value = value;
|
||||
|
||||
_readerWriterLock.ReleaseWriterLock();
|
||||
_readerWriterLock.ExitWriteLock();
|
||||
|
||||
if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
|
||||
{
|
||||
|
@ -12,6 +12,13 @@ namespace Ryujinx.Common.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)
|
||||
|
@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -92,6 +93,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
bool supportsShaderBallot,
|
||||
bool supportsShaderBarrierDivergence,
|
||||
bool supportsShaderFloat64,
|
||||
bool supportsTextureGatherOffsets,
|
||||
bool supportsTextureShadowLod,
|
||||
bool supportsVertexStoreAndAtomics,
|
||||
bool supportsViewportIndexVertexTessellation,
|
||||
@ -142,6 +144,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
SupportsShaderBallot = supportsShaderBallot;
|
||||
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
|
||||
SupportsShaderFloat64 = supportsShaderFloat64;
|
||||
SupportsTextureGatherOffsets = supportsTextureGatherOffsets;
|
||||
SupportsTextureShadowLod = supportsTextureShadowLod;
|
||||
SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;
|
||||
SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation;
|
||||
|
@ -32,6 +32,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
||||
/// </summary>
|
||||
public ref TState State => ref _state.State;
|
||||
|
||||
/// <summary>
|
||||
/// Current shadow state.
|
||||
/// </summary>
|
||||
public ref TState ShadowState => ref _shadowState.State;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the device state, with shadow state.
|
||||
/// </summary>
|
||||
|
@ -1,7 +1,10 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Graphics.Device;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
|
||||
using Ryujinx.Graphics.Gpu.Engine.Threed;
|
||||
using Ryujinx.Graphics.Gpu.Engine.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -15,9 +18,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
private const int ColorLayerCountOffset = 0x818;
|
||||
private const int ColorStructSize = 0x40;
|
||||
private const int ZetaLayerCountOffset = 0x1230;
|
||||
private const int UniformBufferBindVertexOffset = 0x2410;
|
||||
private const int FirstVertexOffset = 0x1434;
|
||||
|
||||
private const int IndirectIndexedDataEntrySize = 0x14;
|
||||
|
||||
private const int LogicOpOffset = 0x19c4;
|
||||
private const int ShaderIdScratchOffset = 0x3470;
|
||||
private const int ShaderAddressScratchOffset = 0x3488;
|
||||
private const int UpdateConstantBufferAddressesBase = 0x34a8;
|
||||
private const int UpdateConstantBufferSizesBase = 0x34bc;
|
||||
private const int UpdateConstantBufferAddressCbu = 0x3460;
|
||||
|
||||
private readonly GPFifoProcessor _processor;
|
||||
private readonly MacroHLEFunctionName _functionName;
|
||||
|
||||
@ -49,6 +61,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
{
|
||||
switch (_functionName)
|
||||
{
|
||||
case MacroHLEFunctionName.BindShaderProgram:
|
||||
BindShaderProgram(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.ClearColor:
|
||||
ClearColor(state, arg0);
|
||||
break;
|
||||
@ -58,6 +73,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
case MacroHLEFunctionName.DrawArraysInstanced:
|
||||
DrawArraysInstanced(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.DrawElements:
|
||||
DrawElements(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.DrawElementsInstanced:
|
||||
DrawElementsInstanced(state, arg0);
|
||||
break;
|
||||
@ -67,6 +85,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
case MacroHLEFunctionName.MultiDrawElementsIndirectCount:
|
||||
MultiDrawElementsIndirectCount(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.UpdateBlendState:
|
||||
UpdateBlendState(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.UpdateColorMasks:
|
||||
UpdateColorMasks(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.UpdateUniformBufferState:
|
||||
UpdateUniformBufferState(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.UpdateUniformBufferStateCbu:
|
||||
UpdateUniformBufferStateCbu(state, arg0);
|
||||
break;
|
||||
case MacroHLEFunctionName.UpdateUniformBufferStateCbuV2:
|
||||
UpdateUniformBufferStateCbuV2(state, arg0);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException(_functionName.ToString());
|
||||
}
|
||||
@ -75,6 +108,149 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
Fifo.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds a shader program with the index in arg0.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void BindShaderProgram(IDeviceState state, int arg0)
|
||||
{
|
||||
int scratchOffset = ShaderIdScratchOffset + arg0 * 4;
|
||||
|
||||
int lastId = state.Read(scratchOffset);
|
||||
int id = FetchParam().Word;
|
||||
int offset = FetchParam().Word;
|
||||
|
||||
if (lastId == id)
|
||||
{
|
||||
FetchParam();
|
||||
FetchParam();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_processor.ThreedClass.SetShaderOffset(arg0, (uint)offset);
|
||||
|
||||
// Removes overflow on the method address into the increment portion.
|
||||
// Present in the original macro.
|
||||
int addrMask = unchecked((int)0xfffc0fff) << 2;
|
||||
|
||||
state.Write(scratchOffset & addrMask, id);
|
||||
state.Write((ShaderAddressScratchOffset + arg0 * 4) & addrMask, offset);
|
||||
|
||||
int stage = FetchParam().Word;
|
||||
uint cbAddress = (uint)FetchParam().Word;
|
||||
|
||||
_processor.ThreedClass.UpdateUniformBufferState(65536, cbAddress >> 24, cbAddress << 8);
|
||||
|
||||
int stageOffset = (stage & 0x7f) << 3;
|
||||
|
||||
state.Write((UniformBufferBindVertexOffset + stageOffset * 4) & addrMask, 17);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates uniform buffer state for update or bind.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void UpdateUniformBufferState(IDeviceState state, int arg0)
|
||||
{
|
||||
uint address = (uint)state.Read(UpdateConstantBufferAddressesBase + arg0 * 4);
|
||||
int size = state.Read(UpdateConstantBufferSizesBase + arg0 * 4);
|
||||
|
||||
_processor.ThreedClass.UpdateUniformBufferState(size, address >> 24, address << 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates uniform buffer state for update.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void UpdateUniformBufferStateCbu(IDeviceState state, int arg0)
|
||||
{
|
||||
uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
|
||||
|
||||
UniformBufferState ubState = new()
|
||||
{
|
||||
Address = new()
|
||||
{
|
||||
High = address >> 24,
|
||||
Low = address << 8
|
||||
},
|
||||
Size = 24320,
|
||||
Offset = arg0 << 2
|
||||
};
|
||||
|
||||
_processor.ThreedClass.UpdateUniformBufferState(ubState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates uniform buffer state for update.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void UpdateUniformBufferStateCbuV2(IDeviceState state, int arg0)
|
||||
{
|
||||
uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
|
||||
|
||||
UniformBufferState ubState = new()
|
||||
{
|
||||
Address = new()
|
||||
{
|
||||
High = address >> 24,
|
||||
Low = address << 8
|
||||
},
|
||||
Size = 28672,
|
||||
Offset = arg0 << 2
|
||||
};
|
||||
|
||||
_processor.ThreedClass.UpdateUniformBufferState(ubState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates blend enable using the given argument.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void UpdateBlendState(IDeviceState state, int arg0)
|
||||
{
|
||||
state.Write(LogicOpOffset, 0);
|
||||
|
||||
Array8<Boolean32> enable = new();
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
enable[i] = new Boolean32((uint)(arg0 >> (i + 8)) & 1);
|
||||
}
|
||||
|
||||
_processor.ThreedClass.UpdateBlendEnable(ref enable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates color masks using the given argument and three pushed arguments.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void UpdateColorMasks(IDeviceState state, int arg0)
|
||||
{
|
||||
Array8<RtColorMask> masks = new();
|
||||
|
||||
int index = 0;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
masks[index++] = new RtColorMask((uint)arg0 & 0x1fff);
|
||||
masks[index++] = new RtColorMask(((uint)arg0 >> 16) & 0x1fff);
|
||||
|
||||
if (i != 3)
|
||||
{
|
||||
arg0 = FetchParam().Word;
|
||||
}
|
||||
}
|
||||
|
||||
_processor.ThreedClass.UpdateColorMasks(ref masks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears one bound color target.
|
||||
/// </summary>
|
||||
@ -129,6 +305,36 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
indexed: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a indexed draw.
|
||||
/// </summary>
|
||||
/// <param name="state">GPU state at the time of the call</param>
|
||||
/// <param name="arg0">First argument of the call</param>
|
||||
private void DrawElements(IDeviceState state, int arg0)
|
||||
{
|
||||
var topology = (PrimitiveTopology)arg0;
|
||||
|
||||
var indexAddressHigh = FetchParam();
|
||||
var indexAddressLow = FetchParam();
|
||||
var indexType = FetchParam();
|
||||
var firstIndex = 0;
|
||||
var indexCount = FetchParam();
|
||||
|
||||
_processor.ThreedClass.UpdateIndexBuffer(
|
||||
(uint)indexAddressHigh.Word,
|
||||
(uint)indexAddressLow.Word,
|
||||
(IndexType)indexType.Word);
|
||||
|
||||
_processor.ThreedClass.Draw(
|
||||
topology,
|
||||
indexCount.Word,
|
||||
1,
|
||||
firstIndex,
|
||||
state.Read(FirstVertexOffset),
|
||||
0,
|
||||
indexed: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a indexed draw.
|
||||
/// </summary>
|
||||
|
@ -6,11 +6,19 @@
|
||||
enum MacroHLEFunctionName
|
||||
{
|
||||
None,
|
||||
BindShaderProgram,
|
||||
ClearColor,
|
||||
ClearDepthStencil,
|
||||
DrawArraysInstanced,
|
||||
DrawElements,
|
||||
DrawElementsInstanced,
|
||||
DrawElementsIndirect,
|
||||
MultiDrawElementsIndirectCount,
|
||||
|
||||
UpdateBlendState,
|
||||
UpdateColorMasks,
|
||||
UpdateUniformBufferState,
|
||||
UpdateUniformBufferStateCbu,
|
||||
UpdateUniformBufferStateCbuV2
|
||||
}
|
||||
}
|
||||
|
@ -46,12 +46,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
|
||||
private static readonly TableEntry[] _table = new TableEntry[]
|
||||
{
|
||||
new(MacroHLEFunctionName.BindShaderProgram, new Hash128(0x5d5efb912369f60b, 0x69131ed5019f08ef), 0x68),
|
||||
new(MacroHLEFunctionName.ClearColor, new Hash128(0xA9FB28D1DC43645A, 0xB177E5D2EAE67FB0), 0x28),
|
||||
new(MacroHLEFunctionName.ClearDepthStencil, new Hash128(0x1B96CB77D4879F4F, 0x8557032FE0C965FB), 0x24),
|
||||
new(MacroHLEFunctionName.DrawArraysInstanced, new Hash128(0x197FB416269DBC26, 0x34288C01DDA82202), 0x48),
|
||||
new(MacroHLEFunctionName.DrawElements, new Hash128(0x3D7F32AE6C2702A7, 0x9353C9F41C1A244D), 0x20),
|
||||
new(MacroHLEFunctionName.DrawElementsInstanced, new Hash128(0x1A501FD3D54EC8E0, 0x6CF570CF79DA74D6), 0x5c),
|
||||
new(MacroHLEFunctionName.DrawElementsIndirect, new Hash128(0x86A3E8E903AF8F45, 0xD35BBA07C23860A4), 0x7c),
|
||||
new(MacroHLEFunctionName.MultiDrawElementsIndirectCount, new Hash128(0x890AF57ED3FB1C37, 0x35D0C95C61F5386F), 0x19C),
|
||||
new(MacroHLEFunctionName.UpdateBlendState, new Hash128(0x40F6D4E7B08D7640, 0x82167BEEAECB959F), 0x28),
|
||||
new(MacroHLEFunctionName.UpdateColorMasks, new Hash128(0x9EE32420B8441DFD, 0x6E7724759A57333E), 0x24),
|
||||
new(MacroHLEFunctionName.UpdateUniformBufferState, new Hash128(0x8EE66706049CB0B0, 0x51C1CF906EC86F7C), 0x20),
|
||||
new(MacroHLEFunctionName.UpdateUniformBufferStateCbu, new Hash128(0xA4592676A3E581A0, 0xA39E77FE19FE04AC), 0x18),
|
||||
new(MacroHLEFunctionName.UpdateUniformBufferStateCbuV2, new Hash128(0x392FA750489983D4, 0x35BACE455155D2C3), 0x18)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -62,18 +69,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
|
||||
/// <returns>True if the host supports the HLE macro, false otherwise</returns>
|
||||
private static bool IsMacroHLESupported(Capabilities caps, MacroHLEFunctionName name)
|
||||
{
|
||||
if (name == MacroHLEFunctionName.ClearColor ||
|
||||
name == MacroHLEFunctionName.ClearDepthStencil ||
|
||||
name == MacroHLEFunctionName.DrawArraysInstanced ||
|
||||
name == MacroHLEFunctionName.DrawElementsInstanced ||
|
||||
name == MacroHLEFunctionName.DrawElementsIndirect)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
|
||||
if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
|
||||
{
|
||||
return caps.SupportsIndirectParameters;
|
||||
}
|
||||
else if (name != MacroHLEFunctionName.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -10,4 +10,22 @@ namespace Ryujinx.Graphics.Gpu.Engine
|
||||
MethodPassthrough = 2,
|
||||
MethodReplay = 3,
|
||||
}
|
||||
|
||||
static class SetMmeShadowRamControlModeExtensions
|
||||
{
|
||||
public static bool IsTrack(this SetMmeShadowRamControlMode mode)
|
||||
{
|
||||
return mode == SetMmeShadowRamControlMode.MethodTrack || mode == SetMmeShadowRamControlMode.MethodTrackWithFilter;
|
||||
}
|
||||
|
||||
public static bool IsPassthrough(this SetMmeShadowRamControlMode mode)
|
||||
{
|
||||
return mode == SetMmeShadowRamControlMode.MethodPassthrough;
|
||||
}
|
||||
|
||||
public static bool IsReplay(this SetMmeShadowRamControlMode mode)
|
||||
{
|
||||
return mode == SetMmeShadowRamControlMode.MethodReplay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
class StateUpdater
|
||||
{
|
||||
public const int ShaderStateIndex = 26;
|
||||
public const int RtColorMaskIndex = 14;
|
||||
public const int RasterizerStateIndex = 15;
|
||||
public const int ScissorStateIndex = 16;
|
||||
public const int VertexBufferStateIndex = 0;
|
||||
public const int BlendStateIndex = 2;
|
||||
public const int IndexBufferStateIndex = 23;
|
||||
public const int PrimitiveRestartStateIndex = 12;
|
||||
public const int RenderTargetStateIndex = 27;
|
||||
|
@ -1,12 +1,15 @@
|
||||
using Ryujinx.Graphics.Device;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Graphics.Device;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
|
||||
using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
|
||||
using Ryujinx.Graphics.Gpu.Engine.Threed.Blender;
|
||||
using Ryujinx.Graphics.Gpu.Engine.Types;
|
||||
using Ryujinx.Graphics.Gpu.Synchronization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
{
|
||||
@ -26,6 +29,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
private readonly ConstantBufferUpdater _cbUpdater;
|
||||
private readonly StateUpdater _stateUpdater;
|
||||
|
||||
private SetMmeShadowRamControlMode ShadowMode => _state.State.SetMmeShadowRamControlMode;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the 3D engine class.
|
||||
/// </summary>
|
||||
@ -228,6 +233,206 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
_cbUpdater.Update(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if two 32 byte structs are equal.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the 32-byte struct</typeparam>
|
||||
/// <param name="lhs">First struct</param>
|
||||
/// <param name="rhs">Second struct</param>
|
||||
/// <returns>True if equal, false otherwise</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool UnsafeEquals32Byte<T>(ref T lhs, ref T rhs) where T : unmanaged
|
||||
{
|
||||
if (Vector256.IsHardwareAccelerated)
|
||||
{
|
||||
return Vector256.EqualsAll(
|
||||
Unsafe.As<T, Vector256<uint>>(ref lhs),
|
||||
Unsafe.As<T, Vector256<uint>>(ref rhs)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
ref var lhsVec = ref Unsafe.As<T, Vector128<uint>>(ref lhs);
|
||||
ref var rhsVec = ref Unsafe.As<T, Vector128<uint>>(ref rhs);
|
||||
|
||||
return Vector128.EqualsAll(lhsVec, rhsVec) &&
|
||||
Vector128.EqualsAll(Unsafe.Add(ref lhsVec, 1), Unsafe.Add(ref rhsVec, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates blend enable. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="masks">Blend enable</param>
|
||||
public void UpdateBlendEnable(ref Array8<Boolean32> enable)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var state = ref _state.State.BlendEnable;
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
enable = _state.ShadowState.BlendEnable;
|
||||
}
|
||||
|
||||
if (!UnsafeEquals32Byte(ref enable, ref state))
|
||||
{
|
||||
state = enable;
|
||||
|
||||
_stateUpdater.ForceDirty(StateUpdater.BlendStateIndex);
|
||||
}
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
_state.ShadowState.BlendEnable = enable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates color masks. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="masks">Color masks</param>
|
||||
public void UpdateColorMasks(ref Array8<RtColorMask> masks)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var state = ref _state.State.RtColorMask;
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
masks = _state.ShadowState.RtColorMask;
|
||||
}
|
||||
|
||||
if (!UnsafeEquals32Byte(ref masks, ref state))
|
||||
{
|
||||
state = masks;
|
||||
|
||||
_stateUpdater.ForceDirty(StateUpdater.RtColorMaskIndex);
|
||||
}
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
_state.ShadowState.RtColorMask = masks;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates index buffer state for an indexed draw. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="addrHigh">High part of the address</param>
|
||||
/// <param name="addrLow">Low part of the address</param>
|
||||
/// <param name="type">Type of the binding</param>
|
||||
public void UpdateIndexBuffer(uint addrHigh, uint addrLow, IndexType type)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var state = ref _state.State.IndexBufferState;
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
ref var shadowState = ref _state.ShadowState.IndexBufferState;
|
||||
addrHigh = shadowState.Address.High;
|
||||
addrLow = shadowState.Address.Low;
|
||||
type = shadowState.Type;
|
||||
}
|
||||
|
||||
if (state.Address.High != addrHigh || state.Address.Low != addrLow || state.Type != type)
|
||||
{
|
||||
state.Address.High = addrHigh;
|
||||
state.Address.Low = addrLow;
|
||||
state.Type = type;
|
||||
|
||||
_stateUpdater.ForceDirty(StateUpdater.IndexBufferStateIndex);
|
||||
}
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
ref var shadowState = ref _state.ShadowState.IndexBufferState;
|
||||
shadowState.Address.High = addrHigh;
|
||||
shadowState.Address.Low = addrLow;
|
||||
shadowState.Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates uniform buffer state for update or bind. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="size">Size of the binding</param>
|
||||
/// <param name="addrHigh">High part of the addrsss</param>
|
||||
/// <param name="addrLow">Low part of the address</param>
|
||||
public void UpdateUniformBufferState(int size, uint addrHigh, uint addrLow)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var state = ref _state.State.UniformBufferState;
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
ref var shadowState = ref _state.ShadowState.UniformBufferState;
|
||||
size = shadowState.Size;
|
||||
addrHigh = shadowState.Address.High;
|
||||
addrLow = shadowState.Address.Low;
|
||||
}
|
||||
|
||||
state.Size = size;
|
||||
state.Address.High = addrHigh;
|
||||
state.Address.Low = addrLow;
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
ref var shadowState = ref _state.ShadowState.UniformBufferState;
|
||||
shadowState.Size = size;
|
||||
shadowState.Address.High = addrHigh;
|
||||
shadowState.Address.Low = addrLow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a shader offset. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of the shader to update</param>
|
||||
/// <param name="offset">Offset to update with</param>
|
||||
public void SetShaderOffset(int index, uint offset)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var shaderState = ref _state.State.ShaderState[index];
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
offset = _state.ShadowState.ShaderState[index].Offset;
|
||||
}
|
||||
|
||||
if (shaderState.Offset != offset)
|
||||
{
|
||||
shaderState.Offset = offset;
|
||||
|
||||
_stateUpdater.ForceDirty(StateUpdater.ShaderStateIndex);
|
||||
}
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
_state.ShadowState.ShaderState[index].Offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates uniform buffer state for update. Respects current shadow mode.
|
||||
/// </summary>
|
||||
/// <param name="ubState">Uniform buffer state</param>
|
||||
public void UpdateUniformBufferState(UniformBufferState ubState)
|
||||
{
|
||||
var shadow = ShadowMode;
|
||||
ref var state = ref _state.State.UniformBufferState;
|
||||
|
||||
if (shadow.IsReplay())
|
||||
{
|
||||
ubState = _state.ShadowState.UniformBufferState;
|
||||
}
|
||||
|
||||
state = ubState;
|
||||
|
||||
if (shadow.IsTrack())
|
||||
{
|
||||
_state.ShadowState.UniformBufferState = ubState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Launches the Inline-to-Memory DMA copy operation.
|
||||
/// </summary>
|
||||
|
@ -590,9 +590,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
/// </summary>
|
||||
struct RtColorMask
|
||||
{
|
||||
#pragma warning disable CS0649 // Field is never assigned to
|
||||
public uint Packed;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
public RtColorMask(uint packed)
|
||||
{
|
||||
Packed = packed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks red channel enable.
|
||||
|
@ -5,9 +5,12 @@
|
||||
/// </summary>
|
||||
readonly struct Boolean32
|
||||
{
|
||||
#pragma warning disable CS0649 // Field is never assigned to
|
||||
private readonly uint _value;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
public Boolean32(uint value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public static implicit operator bool(Boolean32 value)
|
||||
{
|
||||
|
@ -101,6 +101,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool AlwaysFlushOnOverlap { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again.
|
||||
/// </summary>
|
||||
public bool FlushStale { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Increments when the host texture is swapped, or when the texture is removed from all pools.
|
||||
/// </summary>
|
||||
@ -149,6 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool HadPoolOwner { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Physical memory ranges where the texture data is located.
|
||||
/// </summary>
|
||||
public MultiRange Range { get; private set; }
|
||||
@ -1411,6 +1417,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public void SignalModified()
|
||||
{
|
||||
FlushStale = false;
|
||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||
|
||||
if (_modifiedStale || Group.HasCopyDependencies)
|
||||
@ -1431,6 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
if (bound)
|
||||
{
|
||||
FlushStale = false;
|
||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||
}
|
||||
|
||||
@ -1695,12 +1703,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="unmapRange">The range of memory being unmapped</param>
|
||||
public void Unmapped(MultiRange unmapRange)
|
||||
{
|
||||
if (unmapRange.Contains(Range))
|
||||
{
|
||||
// If this is a full unmap, prevent flushes until the texture is mapped again.
|
||||
FlushStale = true;
|
||||
}
|
||||
|
||||
ChangedMapping = true;
|
||||
|
||||
if (Group.Storage == this)
|
||||
{
|
||||
Group.Unmapped();
|
||||
|
||||
Group.ClearModified(unmapRange);
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -226,7 +226,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 +239,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;
|
||||
}
|
||||
@ -374,6 +368,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible;
|
||||
}
|
||||
else if (lhs.Target.IsMultisample() != rhs.Target.IsMultisample() && alignedWidthMatches && lhsAlignedSize.Height == rhsAlignedSize.Height)
|
||||
{
|
||||
// Copy between multisample and non-multisample textures with mismatching size is allowed,
|
||||
// as long aligned size matches.
|
||||
|
||||
return TextureViewCompatibility.CopyOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TextureViewCompatibility.LayoutIncompatible;
|
||||
@ -625,12 +626,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))
|
||||
@ -659,6 +675,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return TextureViewCompatibility.Incompatible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's valid to alias a color format as a depth format.
|
||||
/// </summary>
|
||||
/// <param name="lhsFormat">Source format to be checked</param>
|
||||
/// <param name="rhsFormat">Target format to be checked</param>
|
||||
/// <returns>True if it's valid to alias the formats</returns>
|
||||
private static bool IsValidColorAsDepthAlias(Format lhsFormat, Format rhsFormat)
|
||||
{
|
||||
return (lhsFormat == Format.R32Float && rhsFormat == Format.D32Float) ||
|
||||
(lhsFormat == Format.R16Unorm && rhsFormat == Format.D16Unorm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's valid to alias a depth format as a color format.
|
||||
/// </summary>
|
||||
/// <param name="lhsFormat">Source format to be checked</param>
|
||||
/// <param name="rhsFormat">Target format to be checked</param>
|
||||
/// <returns>True if it's valid to alias the formats</returns>
|
||||
private static bool IsValidDepthAsColorAlias(Format lhsFormat, Format rhsFormat)
|
||||
{
|
||||
return (lhsFormat == Format.D32Float && rhsFormat == Format.R32Float) ||
|
||||
(lhsFormat == Format.D16Unorm && rhsFormat == Format.R16Unorm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if aliasing of two formats that would normally be considered incompatible be allowed,
|
||||
/// using copy dependencies.
|
||||
|
@ -1659,6 +1659,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
return;
|
||||
}
|
||||
|
||||
// If size is zero, we have nothing to flush.
|
||||
// If the flush is stale, we should ignore it because the texture was unmapped since the modified
|
||||
// flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory.
|
||||
if (size == 0 || Storage.FlushStale)
|
||||
{
|
||||
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.
|
||||
|
@ -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 = 5750;
|
||||
private const uint CodeGenVersion = 5791;
|
||||
|
||||
private const string SharedTocFileName = "shared.toc";
|
||||
private const string SharedDataFileName = "shared.data";
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -92,14 +92,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
|
||||
|
||||
private static string GetIndentation(int level)
|
||||
{
|
||||
string indentation = string.Empty;
|
||||
StringBuilder indentationBuilder = new();
|
||||
|
||||
for (int index = 0; index < level; index++)
|
||||
{
|
||||
indentation += Tab;
|
||||
indentationBuilder.Append(Tab);
|
||||
}
|
||||
|
||||
return indentation;
|
||||
return indentationBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using Ryujinx.Graphics.Shader.StructuredIr;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenBallot;
|
||||
using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenCall;
|
||||
@ -67,11 +68,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
|
||||
|
||||
int arity = (int)(info.Type & InstType.ArityMask);
|
||||
|
||||
string args = string.Empty;
|
||||
StringBuilder builder = new();
|
||||
|
||||
if (atomic && (operation.StorageKind == StorageKind.StorageBuffer || operation.StorageKind == StorageKind.SharedMemory))
|
||||
{
|
||||
args = GenerateLoadOrStore(context, operation, isStore: false);
|
||||
builder.Append(GenerateLoadOrStore(context, operation, isStore: false));
|
||||
|
||||
AggregateType dstType = operation.Inst == Instruction.AtomicMaxS32 || operation.Inst == Instruction.AtomicMinS32
|
||||
? AggregateType.S32
|
||||
@ -79,7 +80,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
|
||||
|
||||
for (int argIndex = operation.SourcesCount - arity + 2; argIndex < operation.SourcesCount; argIndex++)
|
||||
{
|
||||
args += ", " + GetSoureExpr(context, operation.GetSource(argIndex), dstType);
|
||||
builder.Append($", {GetSoureExpr(context, operation.GetSource(argIndex), dstType)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -88,16 +89,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
|
||||
{
|
||||
if (argIndex != 0)
|
||||
{
|
||||
args += ", ";
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
AggregateType dstType = GetSrcVarType(inst, argIndex);
|
||||
|
||||
args += GetSoureExpr(context, operation.GetSource(argIndex), dstType);
|
||||
builder.Append(GetSoureExpr(context, operation.GetSource(argIndex), dstType));
|
||||
}
|
||||
}
|
||||
|
||||
return info.OpName + '(' + args + ')';
|
||||
return $"{info.OpName}({builder})";
|
||||
}
|
||||
else if ((info.Type & InstType.Op) != 0)
|
||||
{
|
||||
|
@ -779,17 +779,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
|
||||
|
||||
private static string GetMaskMultiDest(int mask)
|
||||
{
|
||||
string swizzle = ".";
|
||||
StringBuilder swizzleBuilder = new();
|
||||
swizzleBuilder.Append('.');
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if ((mask & (1 << i)) != 0)
|
||||
{
|
||||
swizzle += "xyzw"[i];
|
||||
swizzleBuilder.Append("xyzw"[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return swizzle;
|
||||
return swizzleBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
public StructuredFunction CurrentFunction { get; set; }
|
||||
private readonly Dictionary<AstOperand, Instruction> _locals = new();
|
||||
private readonly Dictionary<int, Instruction[]> _localForArgs = new();
|
||||
private readonly Dictionary<int, Instruction> _funcArgs = new();
|
||||
private readonly Dictionary<int, (StructuredFunction, Instruction)> _functions = new();
|
||||
|
||||
@ -112,7 +111,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
IsMainFunction = isMainFunction;
|
||||
MayHaveReturned = false;
|
||||
_locals.Clear();
|
||||
_localForArgs.Clear();
|
||||
_funcArgs.Clear();
|
||||
}
|
||||
|
||||
@ -169,11 +167,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
_locals.Add(local, spvLocal);
|
||||
}
|
||||
|
||||
public void DeclareLocalForArgs(int funcIndex, Instruction[] spvLocals)
|
||||
{
|
||||
_localForArgs.Add(funcIndex, spvLocals);
|
||||
}
|
||||
|
||||
public void DeclareArgument(int argIndex, Instruction spvLocal)
|
||||
{
|
||||
_funcArgs.Add(argIndex, spvLocal);
|
||||
@ -278,11 +271,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
return _locals[local];
|
||||
}
|
||||
|
||||
public Instruction[] GetLocalForArgsPointers(int funcIndex)
|
||||
{
|
||||
return _localForArgs[funcIndex];
|
||||
}
|
||||
|
||||
public Instruction GetArgumentPointer(AstOperand funcArg)
|
||||
{
|
||||
return _funcArgs[funcArg.Value];
|
||||
|
@ -41,28 +41,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeclareLocalForArgs(CodeGenContext context, List<StructuredFunction> functions)
|
||||
{
|
||||
for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
|
||||
{
|
||||
StructuredFunction function = functions[funcIndex];
|
||||
SpvInstruction[] locals = new SpvInstruction[function.InArguments.Length];
|
||||
|
||||
for (int i = 0; i < function.InArguments.Length; i++)
|
||||
{
|
||||
var type = function.GetArgumentType(i);
|
||||
var localPointerType = context.TypePointer(StorageClass.Function, context.GetType(type));
|
||||
var spvLocal = context.Variable(localPointerType, StorageClass.Function);
|
||||
|
||||
context.AddLocalVariable(spvLocal);
|
||||
|
||||
locals[i] = spvLocal;
|
||||
}
|
||||
|
||||
context.DeclareLocalForArgs(funcIndex, locals);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeclareAll(CodeGenContext context, StructuredProgramInfo info)
|
||||
{
|
||||
DeclareConstantBuffers(context, context.Properties.ConstantBuffers.Values);
|
||||
|
@ -311,26 +311,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
var (function, spvFunc) = context.GetFunction(funcId.Value);
|
||||
|
||||
var args = new SpvInstruction[operation.SourcesCount - 1];
|
||||
var spvLocals = context.GetLocalForArgsPointers(funcId.Value);
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
var operand = operation.GetSource(i + 1);
|
||||
|
||||
if (i >= function.InArguments.Length)
|
||||
{
|
||||
args[i] = context.GetLocalPointer((AstOperand)operand);
|
||||
}
|
||||
else
|
||||
{
|
||||
var type = function.GetArgumentType(i);
|
||||
var value = context.Get(type, operand);
|
||||
var spvLocal = spvLocals[i];
|
||||
|
||||
context.Store(spvLocal, value);
|
||||
|
||||
args[i] = spvLocal;
|
||||
}
|
||||
AstOperand local = (AstOperand)operand;
|
||||
Debug.Assert(local.Type == OperandType.LocalVariable);
|
||||
args[i] = context.GetLocalPointer(local);
|
||||
}
|
||||
|
||||
var retType = function.ReturnType;
|
||||
|
@ -161,7 +161,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
context.EnterBlock(function.MainBlock);
|
||||
|
||||
Declarations.DeclareLocals(context, function);
|
||||
Declarations.DeclareLocalForArgs(context, info.Functions);
|
||||
|
||||
Generate(context, function.MainBlock);
|
||||
|
||||
|
@ -339,6 +339,15 @@ namespace Ryujinx.Graphics.Shader
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries host GPU texture gather with multiple offsets support.
|
||||
/// </summary>
|
||||
/// <returns>True if the GPU and driver supports texture gather offsets, false otherwise</returns>
|
||||
bool QueryHostSupportsTextureGatherOffsets()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries host GPU texture shadow LOD support.
|
||||
/// </summary>
|
||||
|
@ -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)];
|
||||
|
@ -2,17 +2,22 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.StructuredIr
|
||||
{
|
||||
static class StructuredProgram
|
||||
{
|
||||
// TODO: Eventually it should be possible to specify the parameter types for the function instead of using S32 for everything.
|
||||
private const AggregateType FuncParameterType = AggregateType.S32;
|
||||
|
||||
public static StructuredProgramInfo MakeStructuredProgram(
|
||||
IReadOnlyList<Function> functions,
|
||||
AttributeUsage attributeUsage,
|
||||
ShaderDefinitions definitions,
|
||||
ResourceManager resourceManager,
|
||||
TargetLanguage targetLanguage,
|
||||
bool debugMode)
|
||||
{
|
||||
StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode);
|
||||
@ -23,19 +28,19 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
|
||||
|
||||
BasicBlock[] blocks = function.Blocks;
|
||||
|
||||
AggregateType returnType = function.ReturnsValue ? AggregateType.S32 : AggregateType.Void;
|
||||
AggregateType returnType = function.ReturnsValue ? FuncParameterType : AggregateType.Void;
|
||||
|
||||
AggregateType[] inArguments = new AggregateType[function.InArgumentsCount];
|
||||
AggregateType[] outArguments = new AggregateType[function.OutArgumentsCount];
|
||||
|
||||
for (int i = 0; i < inArguments.Length; i++)
|
||||
{
|
||||
inArguments[i] = AggregateType.S32;
|
||||
inArguments[i] = FuncParameterType;
|
||||
}
|
||||
|
||||
for (int i = 0; i < outArguments.Length; i++)
|
||||
{
|
||||
outArguments[i] = AggregateType.S32;
|
||||
outArguments[i] = FuncParameterType;
|
||||
}
|
||||
|
||||
context.EnterFunction(blocks.Length, function.Name, returnType, inArguments, outArguments);
|
||||
@ -58,7 +63,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
|
||||
}
|
||||
else
|
||||
{
|
||||
AddOperation(context, operation);
|
||||
AddOperation(context, operation, targetLanguage, functions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,7 +78,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
|
||||
return context.Info;
|
||||
}
|
||||
|
||||
private static void AddOperation(StructuredProgramContext context, Operation operation)
|
||||
private static void AddOperation(StructuredProgramContext context, Operation operation, TargetLanguage targetLanguage, IReadOnlyList<Function> functions)
|
||||
{
|
||||
Instruction inst = operation.Inst;
|
||||
StorageKind storageKind = operation.StorageKind;
|
||||
@ -114,9 +119,43 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
|
||||
|
||||
IAstNode[] sources = new IAstNode[sourcesCount + outDestsCount];
|
||||
|
||||
for (int index = 0; index < operation.SourcesCount; index++)
|
||||
if (inst == Instruction.Call && targetLanguage == TargetLanguage.Spirv)
|
||||
{
|
||||
sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
|
||||
// SPIR-V requires that all function parameters are copied to a local variable before the call
|
||||
// (or at least that's what the Khronos compiler does).
|
||||
|
||||
// First one is the function index.
|
||||
Operand funcIndexOperand = operation.GetSource(0);
|
||||
Debug.Assert(funcIndexOperand.Type == OperandType.Constant);
|
||||
int funcIndex = funcIndexOperand.Value;
|
||||
|
||||
sources[0] = new AstOperand(OperandType.Constant, funcIndex);
|
||||
|
||||
int inArgsCount = functions[funcIndex].InArgumentsCount;
|
||||
|
||||
// Remaining ones are parameters, copy them to a temp local variable.
|
||||
for (int index = 1; index < operation.SourcesCount; index++)
|
||||
{
|
||||
IAstNode source = context.GetOperandOrCbLoad(operation.GetSource(index));
|
||||
|
||||
if (index - 1 < inArgsCount)
|
||||
{
|
||||
AstOperand argTemp = context.NewTemp(FuncParameterType);
|
||||
context.AddNode(new AstAssignment(argTemp, source));
|
||||
sources[index] = argTemp;
|
||||
}
|
||||
else
|
||||
{
|
||||
sources[index] = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int index = 0; index < operation.SourcesCount; index++)
|
||||
{
|
||||
sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
|
||||
}
|
||||
}
|
||||
|
||||
for (int index = 0; index < outDestsCount; index++)
|
||||
|
@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
|
||||
|
||||
namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
@ -785,30 +786,31 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
|
||||
|
||||
private static string GetFunctionName(Operation baseOp, bool isMultiTarget, IReadOnlyList<uint> targetCbs)
|
||||
{
|
||||
string name = baseOp.Inst.ToString();
|
||||
StringBuilder nameBuilder = new();
|
||||
nameBuilder.Append(baseOp.Inst.ToString());
|
||||
|
||||
name += baseOp.StorageKind switch
|
||||
nameBuilder.Append(baseOp.StorageKind switch
|
||||
{
|
||||
StorageKind.GlobalMemoryS8 => "S8",
|
||||
StorageKind.GlobalMemoryS16 => "S16",
|
||||
StorageKind.GlobalMemoryU8 => "U8",
|
||||
StorageKind.GlobalMemoryU16 => "U16",
|
||||
_ => string.Empty,
|
||||
};
|
||||
});
|
||||
|
||||
if (isMultiTarget)
|
||||
{
|
||||
name += "Multi";
|
||||
nameBuilder.Append("Multi");
|
||||
}
|
||||
|
||||
foreach (uint targetCb in targetCbs)
|
||||
{
|
||||
(int sbCbSlot, int sbCbOffset) = UnpackCbSlotAndOffset(targetCb);
|
||||
|
||||
name += $"_c{sbCbSlot}o{sbCbOffset}";
|
||||
nameBuilder.Append($"_c{sbCbSlot}o{sbCbOffset}");
|
||||
}
|
||||
|
||||
return name;
|
||||
return nameBuilder.ToString();
|
||||
}
|
||||
|
||||
private static bool TryGenerateStorageOp(
|
||||
|
@ -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<INode> 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<INode> 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<INode> node,
|
||||
TextureOperation texOp,
|
||||
|
@ -329,6 +329,7 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
attributeUsage,
|
||||
definitions,
|
||||
resourceManager,
|
||||
Options.TargetLanguage,
|
||||
Options.Flags.HasFlag(TranslationFlags.DebugMode));
|
||||
|
||||
int geometryVerticesPerPrimitive = Definitions.OutputTopology switch
|
||||
|
@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
private int _flushTemp;
|
||||
private int _lastFlushWrite = -1;
|
||||
|
||||
private readonly ReaderWriterLock _flushLock;
|
||||
private readonly ReaderWriterLockSlim _flushLock;
|
||||
private FenceHolder _flushFence;
|
||||
private int _flushWaiting;
|
||||
|
||||
@ -85,7 +85,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_currentType = currentType;
|
||||
DesiredType = currentType;
|
||||
|
||||
_flushLock = new ReaderWriterLock();
|
||||
_flushLock = new ReaderWriterLockSlim();
|
||||
_useMirrors = gd.IsTBDR;
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_currentType = currentType;
|
||||
DesiredType = currentType;
|
||||
|
||||
_flushLock = new ReaderWriterLock();
|
||||
_flushLock = new ReaderWriterLockSlim();
|
||||
}
|
||||
|
||||
public bool TryBackingSwap(ref CommandBufferScoped? cbs)
|
||||
@ -116,7 +116,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
// Only swap if the buffer is not used in any queued command buffer.
|
||||
bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool);
|
||||
|
||||
if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReaderLockHeld && (_pendingData == null || cbs != null))
|
||||
if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null))
|
||||
{
|
||||
var currentAllocation = _allocationAuto;
|
||||
var currentBuffer = _buffer;
|
||||
@ -131,7 +131,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
ClearMirrors(cbs.Value, 0, Size);
|
||||
}
|
||||
|
||||
_flushLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_flushLock.EnterWriteLock();
|
||||
|
||||
ClearFlushFence();
|
||||
|
||||
@ -185,7 +185,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
_gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer);
|
||||
|
||||
_flushLock.ReleaseWriterLock();
|
||||
_flushLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_swapQueued = false;
|
||||
@ -548,42 +548,44 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
private void WaitForFlushFence()
|
||||
{
|
||||
// Assumes the _flushLock is held as reader, returns in same state.
|
||||
if (_flushFence == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If storage has changed, make sure the fence has been reached so that the data is in place.
|
||||
_flushLock.ExitReadLock();
|
||||
_flushLock.EnterWriteLock();
|
||||
|
||||
if (_flushFence != null)
|
||||
{
|
||||
// If storage has changed, make sure the fence has been reached so that the data is in place.
|
||||
var fence = _flushFence;
|
||||
Interlocked.Increment(ref _flushWaiting);
|
||||
|
||||
var cookie = _flushLock.UpgradeToWriterLock(Timeout.Infinite);
|
||||
// Don't wait in the lock.
|
||||
|
||||
if (_flushFence != null)
|
||||
_flushLock.ExitWriteLock();
|
||||
|
||||
fence.Wait();
|
||||
|
||||
_flushLock.EnterWriteLock();
|
||||
|
||||
if (Interlocked.Decrement(ref _flushWaiting) == 0)
|
||||
{
|
||||
var fence = _flushFence;
|
||||
Interlocked.Increment(ref _flushWaiting);
|
||||
|
||||
// Don't wait in the lock.
|
||||
|
||||
var restoreCookie = _flushLock.ReleaseLock();
|
||||
|
||||
fence.Wait();
|
||||
|
||||
_flushLock.RestoreLock(ref restoreCookie);
|
||||
|
||||
if (Interlocked.Decrement(ref _flushWaiting) == 0)
|
||||
{
|
||||
fence.Put();
|
||||
}
|
||||
|
||||
_flushFence = null;
|
||||
fence.Put();
|
||||
}
|
||||
|
||||
_flushLock.DowngradeFromWriterLock(ref cookie);
|
||||
_flushFence = null;
|
||||
}
|
||||
|
||||
// Assumes the _flushLock is held as reader, returns in same state.
|
||||
_flushLock.ExitWriteLock();
|
||||
_flushLock.EnterReadLock();
|
||||
}
|
||||
|
||||
public PinnedSpan<byte> GetData(int offset, int size)
|
||||
{
|
||||
_flushLock.AcquireReaderLock(Timeout.Infinite);
|
||||
_flushLock.EnterReadLock();
|
||||
|
||||
WaitForFlushFence();
|
||||
|
||||
@ -603,7 +605,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
// Need to be careful here, the buffer can't be unmapped while the data is being used.
|
||||
_buffer.IncrementReferenceCount();
|
||||
|
||||
_flushLock.ReleaseReaderLock();
|
||||
_flushLock.ExitReadLock();
|
||||
|
||||
return PinnedSpan<byte>.UnsafeFromSpan(result, _buffer.DecrementReferenceCount);
|
||||
}
|
||||
@ -621,7 +623,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
result = resource.GetFlushBuffer().GetBufferData(resource.GetPool(), this, offset, size);
|
||||
}
|
||||
|
||||
_flushLock.ReleaseReaderLock();
|
||||
_flushLock.ExitReadLock();
|
||||
|
||||
// Flush buffer is pinned until the next GetBufferData on the thread, which is fine for current uses.
|
||||
return PinnedSpan<byte>.UnsafeFromSpan(result);
|
||||
@ -1073,11 +1075,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_allocationAuto.Dispose();
|
||||
}
|
||||
|
||||
_flushLock.AcquireWriterLock(Timeout.Infinite);
|
||||
_flushLock.EnterWriteLock();
|
||||
|
||||
ClearFlushFence();
|
||||
|
||||
_flushLock.ReleaseWriterLock();
|
||||
_flushLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -19,6 +19,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.HLE.FileSystem
|
||||
@ -197,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
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)
|
||||
{
|
||||
@ -209,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem
|
||||
using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
|
||||
using var cnmtFile = new UniqueRef<IFile>();
|
||||
|
||||
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)
|
||||
@ -219,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,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);
|
||||
}
|
||||
@ -258,17 +260,17 @@ namespace Ryujinx.HLE.FileSystem
|
||||
{
|
||||
var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
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?
|
||||
@ -605,11 +607,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();
|
||||
@ -817,13 +819,13 @@ namespace Ryujinx.HLE.FileSystem
|
||||
|
||||
if (updateNcas.Count > 0)
|
||||
{
|
||||
string extraNcas = string.Empty;
|
||||
StringBuilder extraNcas = new();
|
||||
|
||||
foreach (var entry in updateNcas)
|
||||
{
|
||||
foreach (var (type, path) in entry.Value)
|
||||
{
|
||||
extraNcas += path + Environment.NewLine;
|
||||
extraNcas.AppendLine(path);
|
||||
}
|
||||
}
|
||||
|
||||
@ -954,13 +956,13 @@ namespace Ryujinx.HLE.FileSystem
|
||||
|
||||
if (updateNcas.Count > 0)
|
||||
{
|
||||
string extraNcas = string.Empty;
|
||||
StringBuilder extraNcas = new();
|
||||
|
||||
foreach (var entry in updateNcas)
|
||||
{
|
||||
foreach (var (type, path) in entry.Value)
|
||||
{
|
||||
extraNcas += path + Environment.NewLine;
|
||||
extraNcas.AppendLine(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ulong, Stream> _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)
|
||||
|
@ -101,7 +101,7 @@ namespace Ryujinx.HLE
|
||||
/// <summary>
|
||||
/// Control if the guest application should be told that there is a Internet connection available.
|
||||
/// </summary>
|
||||
internal readonly bool EnableInternetAccess;
|
||||
public bool EnableInternetAccess { internal get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Control LibHac's integrity check level.
|
||||
|
@ -436,14 +436,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
|
||||
uint nameIndex = sym.NameOffset;
|
||||
|
||||
string name = string.Empty;
|
||||
StringBuilder nameBuilder = new();
|
||||
|
||||
for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
|
||||
{
|
||||
name += (char)chr;
|
||||
nameBuilder.Append((char)chr);
|
||||
}
|
||||
|
||||
return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
|
||||
return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
|
||||
}
|
||||
|
||||
private static ElfSymbol GetSymbol32(IVirtualMemoryManager memory, ulong address, ulong strTblAddr)
|
||||
@ -452,14 +452,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
|
||||
uint nameIndex = sym.NameOffset;
|
||||
|
||||
string name = string.Empty;
|
||||
StringBuilder nameBuilder = new();
|
||||
|
||||
for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
|
||||
{
|
||||
name += (char)chr;
|
||||
nameBuilder.Append((char)chr);
|
||||
}
|
||||
|
||||
return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
|
||||
return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
try
|
||||
{
|
||||
LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
|
||||
using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(new PartitionFileSystem(storage));
|
||||
var pfs = new PartitionFileSystem();
|
||||
using SharedRef<LibHac.Fs.Fsa.IFileSystem> 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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)]
|
||||
|
@ -1,8 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Ins
|
||||
{
|
||||
[Service("ins:r")]
|
||||
class IReceiverManager : IpcService
|
||||
{
|
||||
public IReceiverManager(ServiceCtx context) { }
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Ins
|
||||
{
|
||||
[Service("ins:s")]
|
||||
class ISenderManager : IpcService
|
||||
{
|
||||
public ISenderManager(ServiceCtx context) { }
|
||||
}
|
||||
}
|
12
src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
Normal file
12
src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
|
@ -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<byte>();
|
||||
|
||||
if (i < 8)
|
||||
if (i < LdnConst.NodeCountMax)
|
||||
{
|
||||
result[i].State = array[i].State;
|
||||
array[i].State = NodeLatestUpdateFlags.None;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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<NetworkChangeEventArgs> NetworkChange;
|
||||
|
||||
void DisconnectNetwork();
|
@ -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;
|
||||
@ -29,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
private const bool IsDevelopment = false;
|
||||
|
||||
private readonly KEvent _stateChangeEvent;
|
||||
private int _stateChangeEventHandle;
|
||||
|
||||
private NetworkState _state;
|
||||
private DisconnectReason _disconnectReason;
|
||||
@ -277,12 +278,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
// AttachStateChangeEvent() -> handle<copy>
|
||||
public ResultCode AttachStateChangeEvent(ServiceCtx context)
|
||||
{
|
||||
if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out int stateChangeEventHandle) != Result.Success)
|
||||
if (_stateChangeEventHandle == 0 && context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException("Out of handles!");
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(stateChangeEventHandle);
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle);
|
||||
|
||||
// Returns ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
|
||||
|
||||
@ -394,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;
|
||||
@ -545,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
|
||||
NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
|
||||
|
||||
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;
|
||||
@ -554,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;
|
||||
}
|
||||
@ -567,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)
|
||||
{
|
||||
@ -677,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;
|
||||
}
|
||||
@ -847,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
context.Memory.Read(bufferPosition, networkInfoBytes);
|
||||
|
||||
networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0];
|
||||
networkInfo = MemoryMarshal.Read<NetworkInfo>(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;
|
||||
@ -859,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;
|
||||
}
|
||||
@ -964,6 +965,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
SetDisconnectReason(DisconnectReason.None);
|
||||
}
|
||||
|
||||
if (_stateChangeEventHandle != 0)
|
||||
{
|
||||
context.Process.HandleTable.CloseHandle(_stateChangeEventHandle);
|
||||
_stateChangeEventHandle = 0;
|
||||
}
|
||||
|
||||
return resultCode;
|
||||
}
|
||||
|
||||
@ -1021,7 +1028,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
SetState(NetworkState.None);
|
||||
|
||||
NetworkClient?.DisconnectAndStop();
|
||||
NetworkClient?.Dispose();
|
||||
NetworkClient = null;
|
||||
|
||||
return ResultCode.Success;
|
||||
@ -1054,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;
|
||||
}
|
||||
|
||||
@ -1072,7 +1085,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
}
|
||||
else
|
||||
{
|
||||
// NOTE: Service returns differents ResultCode here related to the nifm ResultCode.
|
||||
// NOTE: Service returns different ResultCode here related to the nifm ResultCode.
|
||||
resultCode = ResultCode.DeviceDisabled;
|
||||
_nifmResultCode = resultCode;
|
||||
}
|
||||
@ -1084,14 +1097,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (NetworkClient != null)
|
||||
{
|
||||
_station?.Dispose();
|
||||
_accessPoint?.Dispose();
|
||||
_station?.Dispose();
|
||||
_station = null;
|
||||
|
||||
NetworkClient.DisconnectAndStop();
|
||||
}
|
||||
_accessPoint?.Dispose();
|
||||
_accessPoint = null;
|
||||
|
||||
NetworkClient?.Dispose();
|
||||
NetworkClient = null;
|
||||
}
|
||||
}
|
||||
|
@ -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<NetworkChangeEventArgs> NetworkChange;
|
||||
|
||||
public NetworkError Connect(ConnectRequest request)
|
@ -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<LdnProxyTcpSession> _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<byte>(),
|
||||
},
|
||||
Common = new CommonNetworkInfo
|
||||
{
|
||||
MacAddress = new Array6<byte>(),
|
||||
Ssid = new Ssid
|
||||
{
|
||||
Name = new Array33<byte>(),
|
||||
},
|
||||
},
|
||||
Ldn = new LdnNetworkInfo
|
||||
{
|
||||
NodeCountMax = LdnConst.NodeCountMax,
|
||||
SecurityParameter = new Array16<byte>(),
|
||||
Nodes = new Array8<NodeInfo>(),
|
||||
AdvertiseData = new Array384<byte>(),
|
||||
Reserved4 = new Array140<byte>(),
|
||||
},
|
||||
};
|
||||
|
||||
for (int i = 0; i < LdnConst.NodeCountMax; i++)
|
||||
{
|
||||
networkInfo.Ldn.Nodes[i] = new NodeInfo
|
||||
{
|
||||
MacAddress = new Array6<byte>(),
|
||||
UserName = new Array33<byte>(),
|
||||
Reserved2 = new Array16<byte>(),
|
||||
};
|
||||
}
|
||||
|
||||
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<byte>(),
|
||||
UserName = new Array33<byte>(),
|
||||
Reserved2 = new Array16<byte>(),
|
||||
};
|
||||
|
||||
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<NodeInfo>();
|
||||
|
||||
for (int i = 0; i < LdnConst.NodeCountMax; i++)
|
||||
{
|
||||
NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
|
||||
NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Array6<byte> GetFakeMac(IPAddress address = null)
|
||||
{
|
||||
address ??= LocalAddr;
|
||||
|
||||
byte[] ip = address.GetAddressBytes();
|
||||
|
||||
var macAddress = new Array6<byte>();
|
||||
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<NetworkInfo>();
|
||||
}
|
||||
|
||||
List<NetworkInfo> outNetworkInfo = new();
|
||||
|
||||
foreach (KeyValuePair<ulong, NetworkInfo> 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<byte> gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
|
||||
Span<byte> 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<NodeInfo> 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<NetworkInfo, byte>(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<byte>();
|
||||
_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<NodeInfo, byte>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<LanPacketHeader>();
|
||||
|
||||
private readonly LanDiscovery _discovery;
|
||||
|
||||
public event Action<LdnProxyTcpSession> Accept;
|
||||
public event Action<EndPoint, LanPacketType, byte[]> Scan;
|
||||
public event Action<NetworkInfo> ScanResponse;
|
||||
public event Action<NetworkInfo> SyncNetwork;
|
||||
public event Action<NodeInfo, EndPoint> Connect;
|
||||
public event Action<LdnProxyTcpSession> 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<NetworkInfo, byte>(ref _discovery.NetworkInfo).ToArray());
|
||||
}
|
||||
break;
|
||||
case LanPacketType.ScanResponse:
|
||||
// UDP
|
||||
ScanResponse?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
|
||||
break;
|
||||
case LanPacketType.SyncNetwork:
|
||||
// TCP
|
||||
SyncNetwork?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
|
||||
break;
|
||||
case LanPacketType.Connect:
|
||||
// TCP Session / Station
|
||||
Connect?.Invoke(MemoryMarshal.Cast<byte, NodeInfo>(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<byte, LanPacketHeader>(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<byte>(), 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<byte>();
|
||||
|
||||
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<LanPacketHeader, byte>(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<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
|
||||
data.CopyTo(buf, _headerSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf = new byte[_headerSize];
|
||||
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private int Compress(byte[] input, out byte[] output)
|
||||
{
|
||||
List<byte> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Client implementation for <a href="https://github.com/spacemeowx2/ldn_mitm">ldn_mitm</a>
|
||||
/// </summary>
|
||||
internal class LdnMitmClient : INetworkClient
|
||||
{
|
||||
public bool NeedsRealId => false;
|
||||
|
||||
public event EventHandler<NetworkChangeEventArgs> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||
{
|
||||
internal interface ILdnTcpSocket : ILdnSocket
|
||||
{
|
||||
bool Connect();
|
||||
void DisconnectAndStop();
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ulong, NetworkInfo> _scanResultsLast = new();
|
||||
private Dictionary<ulong, NetworkInfo> _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<byte> 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<ulong, NetworkInfo> 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<ulong, NetworkInfo>();
|
||||
|
||||
foreach (KeyValuePair<ulong, NetworkInfo> last in _scanResultsLast)
|
||||
{
|
||||
results[last.Key] = last.Value;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<ulong, NetworkInfo> scan in _scanResults)
|
||||
{
|
||||
results[scan.Key] = scan.Value;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<byte> Reserved;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
|
||||
{
|
||||
internal enum LanPacketType : byte
|
||||
{
|
||||
Scan,
|
||||
ScanResponse,
|
||||
Connect,
|
||||
SyncNetwork,
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user