Compare commits

...

12 Commits

Author SHA1 Message Date
riperiperi
1be668e68a HLE: Add OS-specific precise sleep methods to reduce spinwaiting (#5948)
* feat: add nanosleep for linux and macos

* Add Windows 0.5ms sleep

- Imprecise waits for longer waits with clock alignment
- 1/4 the spin time on vsync timer

* Remove old experiment

* Fix event leak

* Tweaking for MacOS

* Linux tweaks, nanosleep vsync improvement

* Fix overbias

* Cleanup

* Fix realignment

* Add some docs and some cleanup

NanosleepPool needs more, Nanosleep has some benchmark code that needs removed.

* Rename "Microsleep" to "PreciseSleep"

Might have been confused with "microseconds", which no measurement is performed in.

* Remove nanosleep measurement

* Remove unused debug logging

* Nanosleep Pool Documentation

* More cleanup

* Whitespace

* Formatting

* Address Feedback

* Allow SleepUntilTimePoint to take EventWaitHandle

* Remove `_chrono` stopwatch in SurfaceFlinger

* Move spinwaiting logic to PreciseSleepHelper

Technically, these achieve different things, but having them here makes them easier to reuse or tune.
2023-11-30 15:39:42 -03:00
gdkchan
21cd4c0c00 Extend bindless elimination to see through shuffle (#5958)
* Extend bindless elimination to see through shuffle

* Shader cache version bump
2023-11-23 00:51:51 +01:00
gdkchan
70d65d3d8e Enable copy dependency between RGBA8 and RGBA32 formats (#5943)
* Enable copy dependency between RGBA8 and RGBA32 formats

* Take packed flag into account for texture formats

* Account for widths not being a multiple of each other

* Don't try to alias depth textures as color, fix log condition

* PR feedback
2023-11-19 15:27:34 -03:00
gdkchan
0b58f46266 Extend bindless elimination to see through Phis with the same results (#5957)
* Extend bindless elimination to see through Phis with the same results

* Shader cache version bump
2023-11-19 15:10:44 -03:00
MutantAura
aa96dcb1be misc: Default to Vulkan if available or running on macOS (#5913)
* Addition of default backend check. Vulkan is preferred if available or macOS.

* import ordering format fix

* Update src/Ryujinx/Program.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* remove redundant load types

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-11-18 17:42:45 -03:00
gdkchan
82a638230e Fix JitCache.Unmap called with the same address freeing memory in use (#5937) 2023-11-16 17:52:21 -03:00
Isaac Marovitz
d11fe26aa3 Fix macOS Path (#5941) 2023-11-16 16:09:15 -03:00
gdkchan
dcf10561b9 Fix missing texture flush for draw then DMA copy sequence without render target change (#5933)
* Unbind render targets before DMA copy

* Move DirtyAction to TextureGroupHandle

* Fix lost copy dependency bug

* XML doc
2023-11-15 21:36:25 -03:00
Mary Guillemard
cdc8fed64f chore: Update OpenTK to 4.8.1 (#5912)
OpenTK.OpenAL was renamed to OpenTK.Audio.OpenAL.

Signed-off-by: Mary Guillemard <mary@mary.zone>
2023-11-15 19:08:46 +01:00
Mary Guillemard
388446c255 infra: Workaround Microsoft.NET.ILLink.Tasks restore failure on Flathub
This package seems to be required for triming now but isn't restored by default.
This changes the flatpak pusher to publish so we are sure that the package is in the cache.

Signed-off-by: Mary Guillemard <mary@mary.zone>
2023-11-15 18:12:19 +01:00
Zoltan Csizmadia
29e192f241 Migrate to .NET 8 (#5887)
* Change TargetFramework to net8.0

* Disable info messages

* Fix warings

* Disable additional analyzer messages

* Fix typo

* Add whitespace

* Fix ref vs in warnings

* Use explicit [In] on array parameters

* No need to guard Remove with Contains

* Use 'ArgumentOutOfRangeException.ThrowIf...' instead of explicitly throwing a new exception instance

* Bump .NET SDK version

* Enable JsonSerializerIsReflectionEnabledByDefault

* Use 8.0.100 GA release

* Bump System package versions

---------

Co-authored-by: Zoltan Csizmadia <Zoltan.Csizmadia@vericast.com>
2023-11-15 17:41:31 +01:00
gdkchan
5b3662b793 Disable DMA GPU copy for block linear to linear copies (#5927)
* Disable DMA GPU copy for block linear to linear copies

* Simplify check

* PR feedback
2023-11-14 23:24:42 -03:00
84 changed files with 1377 additions and 333 deletions

View File

@@ -233,6 +233,29 @@ dotnet_naming_style.IPascalCase.required_suffix =
dotnet_naming_style.IPascalCase.word_separator =
dotnet_naming_style.IPascalCase.capitalization = pascal_case
# TODO:
# .NET 8 migration (new warnings are caused by the NET 8 C# compiler and analyzer)
# The following info messages might need to be fixed in the source code instead of hiding the actual message
# Without the following lines, dotnet format would fail
# Disable "Collection initialization can be simplified"
dotnet_diagnostic.IDE0028.severity = none
dotnet_diagnostic.IDE0300.severity = none
dotnet_diagnostic.IDE0301.severity = none
dotnet_diagnostic.IDE0302.severity = none
dotnet_diagnostic.IDE0305.severity = none
# Disable "'new' expression can be simplified"
dotnet_diagnostic.IDE0090.severity = none
# Disable "Use primary constructor"
dotnet_diagnostic.IDE0290.severity = none
# Disable "Member '' does not access instance data and can be marked as static"
dotnet_diagnostic.CA1822.severity = none
# Disable "Change type of field '' from '' to '' for improved performance"
dotnet_diagnostic.CA1859.severity = none
# Disable "Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array"
dotnet_diagnostic.CA1861.severity = none
# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'"
dotnet_diagnostic.CA1862.severity = none
[src/Ryujinx.HLE/HOS/Services/**.cs]
# Disable "mark members as static" rule for services
dotnet_diagnostic.CA1822.severity = none

View File

@@ -30,7 +30,7 @@ jobs:
- os: windows-latest
OS_NAME: Windows x64
DOTNET_RUNTIME_IDENTIFIER: win10-x64
DOTNET_RUNTIME_IDENTIFIER: win-x64
RELEASE_ZIP_OS_NAME: win_x64
fail-fast: false

View File

@@ -49,7 +49,9 @@ jobs:
run: python -m pip install PyYAML lxml
- name: Restore Nuget packages
run: dotnet restore Ryujinx/${{ env.RYUJINX_PROJECT_FILE }}
# With .NET 8.0.100, Microsoft.NET.ILLink.Tasks isn't restored by default and only seems to appears when publishing.
# So we just publish to grab the dependencies
run: dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained
- name: Generate nuget_sources.json
shell: python

View File

@@ -59,7 +59,7 @@ jobs:
- os: windows-latest
OS_NAME: Windows x64
DOTNET_RUNTIME_IDENTIFIER: win10-x64
DOTNET_RUNTIME_IDENTIFIER: win-x64
RELEASE_ZIP_OS_NAME: win_x64
steps:
- uses: actions/checkout@v4

View File

@@ -21,16 +21,16 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<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" />
<PackageVersion Include="OpenTK.Graphics" Version="4.7.7" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.7.7" />
<PackageVersion Include="OpenTK.Core" Version="4.8.1" />
<PackageVersion Include="OpenTK.Graphics" Version="4.8.1" />
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.1" />
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.1" />
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build13" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
@@ -45,10 +45,10 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
<PackageVersion Include="SPB" Version="0.0.4-build28" />
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
<PackageVersion Include="System.IO.Hashing" Version="7.0.0" />
<PackageVersion Include="System.Management" Version="7.0.2" />
<PackageVersion Include="System.Drawing.Common" Version="8.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>

View File

@@ -68,7 +68,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of
If you wish to build the emulator yourself, follow these steps:
### Step 1
Install the X64 version of [.NET 7.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/7.0).
Install the X64 version of [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
### Step 2
Either use `git clone https://github.com/Ryujinx/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files.

View File

@@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.200",
"version": "8.0.100",
"rollForward": "latestFeature"
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -117,12 +117,11 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
bool result = TryFind(funcOffset, out CacheEntry entry);
Debug.Assert(result);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
Remove(funcOffset);
_cacheEntries.RemoveAt(entryIndex);
}
}
}
@@ -181,22 +180,7 @@ namespace ARMeilleure.Translation.Cache
_cacheEntries.Insert(index, entry);
}
private static void Remove(int offset)
{
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
if (index < 0)
{
index = ~index - 1;
}
if (index >= 0)
{
_cacheEntries.RemoveAt(index);
}
}
public static bool TryFind(int offset, out CacheEntry entry)
public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
@@ -210,11 +194,13 @@ namespace ARMeilleure.Translation.Cache
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
}
entry = default;
entryIndex = 0;
return false;
}
}

View File

@@ -95,7 +95,7 @@ namespace ARMeilleure.Translation.Cache
{
int offset = (int)((long)controlPc - context.ToInt64());
if (!JitCache.TryFind(offset, out CacheEntry funcEntry))
if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _))
{
return null; // Not found.
}

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK.OpenAL" />
<PackageReference Include="OpenTK.Audio.OpenAL" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifiers>win10-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
@@ -15,11 +15,11 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dll</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'">
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dylib</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win10-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.so</TargetPath>
</ContentWithTargetPath>

View File

@@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Utils
throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null);
}
MemoryMarshal.Write(backingMemory.Span[..size], ref data);
MemoryMarshal.Write(backingMemory.Span[..size], in data);
backingMemory = backingMemory[size..];
}
@@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Utils
throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null);
}
MemoryMarshal.Write(backingMemory[..size], ref data);
MemoryMarshal.Write(backingMemory[..size], in data);
backingMemory = backingMemory[size..];
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
@@ -25,6 +25,16 @@
<TrimMode>partial</TrimMode>
</PropertyGroup>
<!--
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
See:
https://github.com/amwx/FluentAvalonia/issues/481
https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/
-->
<PropertyGroup>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
@@ -40,7 +50,7 @@
<PackageReference Include="OpenTK.Core" />
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
<PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />

View File

@@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Helpers
public static partial IntPtr SetCursor(IntPtr handle);
[LibraryImport("user32.dll")]
public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvAndPlane, byte[] pvXorPlane);
public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, [In] byte[] pvAndPlane, [In] byte[] pvXorPlane);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
public static partial ushort RegisterClassEx(ref WndClassEx param);

View File

@@ -48,7 +48,7 @@ namespace Ryujinx.Common.Configuration
string appDataPath;
if (OperatingSystem.IsMacOS())
{
appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support");
appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support");
}
else
{

View File

@@ -0,0 +1,38 @@
using System;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// An event which works similarly to an AutoResetEvent, but is backed by a
/// more precise timer that allows waits of less than a millisecond.
/// </summary>
public interface IPreciseSleepEvent : IDisposable
{
/// <summary>
/// Adjust a timepoint to better fit the host clock.
/// When no adjustment is made, the input timepoint will be returned.
/// </summary>
/// <param name="timePoint">Timepoint to adjust</param>
/// <param name="timeoutNs">Requested timeout in nanoseconds</param>
/// <returns>Adjusted timepoint</returns>
long AdjustTimePoint(long timePoint, long timeoutNs);
/// <summary>
/// Sleep until a timepoint, or a signal is received.
/// Given no signal, may wake considerably before, or slightly after the timeout.
/// </summary>
/// <param name="timePoint">Timepoint to sleep until</param>
/// <returns>True if signalled or waited, false if a wait could not be performed</returns>
bool SleepUntil(long timePoint);
/// <summary>
/// Sleep until a signal is received.
/// </summary>
void Sleep();
/// <summary>
/// Signal the event, waking any sleeping thread or the next attempted sleep.
/// </summary>
void Signal();
}
}

View File

@@ -0,0 +1,160 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// Access to Linux/MacOS nanosleep, with platform specific bias to improve precision.
/// </summary>
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("android")]
[SupportedOSPlatform("ios")]
internal static partial class Nanosleep
{
private const long LinuxBaseNanosleepBias = 50000; // 0.05ms
// Penalty for max allowed sleep duration
private const long LinuxNanosleepAccuracyPenaltyThreshold = 200000; // 0.2ms
private const long LinuxNanosleepAccuracyPenalty = 30000; // 0.03ms
// Penalty for base sleep duration
private const long LinuxNanosleepBasePenaltyThreshold = 500000; // 0.5ms
private const long LinuxNanosleepBasePenalty = 30000; // 0.03ms
private const long LinuxNanosleepPenaltyPerMillisecond = 18000; // 0.018ms
private const long LinuxNanosleepPenaltyCap = 18000; // 0.018ms
private const long LinuxStrictBiasOffset = 150_000; // 0.15ms
// Nanosleep duration is biased depending on the requested timeout on MacOS.
// These match the results when measuring on an M1 processor at AboveNormal priority.
private const long MacosBaseNanosleepBias = 5000; // 0.005ms
private const long MacosBiasPerMillisecond = 140000; // 0.14ms
private const long MacosBiasMaxNanoseconds = 20_000_000; // 20ms
private const long MacosStrictBiasOffset = 150_000; // 0.15ms
public static long Bias { get; }
/// <summary>
/// Get bias for a given nanosecond timeout.
/// Some platforms calculate their bias differently, this method can be used to counteract it.
/// </summary>
/// <param name="timeoutNs">Nanosecond timeout</param>
/// <returns>Bias in nanoseconds</returns>
public static long GetBias(long timeoutNs)
{
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{
long biasNs = Math.Min(timeoutNs, MacosBiasMaxNanoseconds);
return MacosBaseNanosleepBias + biasNs * MacosBiasPerMillisecond / 1_000_000;
}
else
{
long bias = LinuxBaseNanosleepBias;
if (timeoutNs > LinuxNanosleepBasePenaltyThreshold)
{
long penalty = (timeoutNs - LinuxNanosleepBasePenaltyThreshold) * LinuxNanosleepPenaltyPerMillisecond / 1_000_000;
bias += LinuxNanosleepBasePenalty + Math.Min(LinuxNanosleepPenaltyCap, penalty);
}
return bias;
}
}
/// <summary>
/// Get a stricter bias for a given nanosecond timeout,
/// which can improve the chances the sleep completes before the timeout.
/// Some platforms calculate their bias differently, this method can be used to counteract it.
/// </summary>
/// <param name="timeoutNs">Nanosecond timeout</param>
/// <returns>Strict bias in nanoseconds</returns>
public static long GetStrictBias(long timeoutNs)
{
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{
return GetBias(timeoutNs) + MacosStrictBiasOffset;
}
else
{
long bias = GetBias(timeoutNs) + LinuxStrictBiasOffset;
if (timeoutNs > LinuxNanosleepAccuracyPenaltyThreshold)
{
bias += LinuxNanosleepAccuracyPenalty;
}
return bias;
}
}
static Nanosleep()
{
Bias = GetBias(0);
}
[StructLayout(LayoutKind.Sequential)]
private struct Timespec
{
public long tv_sec; // Seconds
public long tv_nsec; // Nanoseconds
}
[LibraryImport("libc", SetLastError = true)]
private static partial int nanosleep(ref Timespec req, ref Timespec rem);
/// <summary>
/// Convert a timeout in nanoseconds to a timespec for nanosleep.
/// </summary>
/// <param name="nanoseconds">Timeout in nanoseconds</param>
/// <returns>Timespec for nanosleep</returns>
private static Timespec GetTimespecFromNanoseconds(ulong nanoseconds)
{
return new Timespec
{
tv_sec = (long)(nanoseconds / 1_000_000_000),
tv_nsec = (long)(nanoseconds % 1_000_000_000)
};
}
/// <summary>
/// Sleep for approximately a given time period in nanoseconds.
/// </summary>
/// <param name="nanoseconds">Time to sleep for in nanoseconds</param>
public static void Sleep(long nanoseconds)
{
nanoseconds -= GetBias(nanoseconds);
if (nanoseconds >= 0)
{
Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
Timespec rem = new();
nanosleep(ref req, ref rem);
}
}
/// <summary>
/// Sleep for at most a given time period in nanoseconds.
/// Uses a stricter bias to wake before the requested duration.
/// </summary>
/// <remarks>
/// Due to OS scheduling behaviour, this timeframe may still be missed.
/// </remarks>
/// <param name="nanoseconds">Maximum allowed time for sleep</param>
public static void SleepAtMost(long nanoseconds)
{
// Stricter bias to ensure we wake before the timepoint.
nanoseconds -= GetStrictBias(nanoseconds);
if (nanoseconds >= 0)
{
Timespec req = GetTimespecFromNanoseconds((ulong)nanoseconds);
Timespec rem = new();
nanosleep(ref req, ref rem);
}
}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// A precise sleep event for linux and macos that uses nanosleep for more precise timeouts.
/// </summary>
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("android")]
[SupportedOSPlatform("ios")]
internal class NanosleepEvent : IPreciseSleepEvent
{
private readonly AutoResetEvent _waitEvent = new(false);
private readonly NanosleepPool _pool;
public NanosleepEvent()
{
_pool = new NanosleepPool(_waitEvent);
}
public long AdjustTimePoint(long timePoint, long timeoutNs)
{
// No adjustment
return timePoint;
}
public bool SleepUntil(long timePoint)
{
long now = PerformanceCounter.ElapsedTicks;
long delta = (timePoint - now);
long ms = Math.Min(delta / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
long ns = (delta * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
if (ms > 0)
{
_waitEvent.WaitOne((int)ms);
return true;
}
else if (ns - Nanosleep.Bias > 0)
{
// Don't bother starting a sleep if there's already a signal active.
if (_waitEvent.WaitOne(0))
{
return true;
}
// The 1ms wait will be interrupted by the nanosleep timeout if it completes.
if (!_pool.SleepAndSignal(ns, timePoint))
{
// Too many threads on the pool.
return false;
}
_waitEvent.WaitOne(1);
_pool.IgnoreSignal();
return true;
}
return false;
}
public void Sleep()
{
_waitEvent.WaitOne();
}
public void Signal()
{
_waitEvent.Set();
}
public void Dispose()
{
GC.SuppressFinalize(this);
_pool.Dispose();
_waitEvent.Dispose();
}
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// A pool of threads used to allow "interruptable" nanosleep for a single target event.
/// </summary>
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("android")]
[SupportedOSPlatform("ios")]
internal class NanosleepPool : IDisposable
{
public const int MaxThreads = 8;
/// <summary>
/// A thread that nanosleeps and may signal an event on wake.
/// When a thread is assigned a nanosleep to perform, it also gets a signal ID.
/// The pool's target event is only signalled if this ID matches the latest dispatched one.
/// </summary>
private class NanosleepThread : IDisposable
{
private static readonly long _timePointEpsilon;
static NanosleepThread()
{
_timePointEpsilon = PerformanceCounter.TicksPerMillisecond / 100; // 0.01ms
}
private readonly Thread _thread;
private readonly NanosleepPool _parent;
private readonly AutoResetEvent _newWaitEvent;
private bool _running = true;
private long _signalId;
private long _nanoseconds;
private long _timePoint;
public long SignalId => _signalId;
/// <summary>
/// Creates a new NanosleepThread for a parent pool, with a specified thread ID.
/// </summary>
/// <param name="parent">Parent NanosleepPool</param>
/// <param name="id">Thread ID</param>
public NanosleepThread(NanosleepPool parent, int id)
{
_parent = parent;
_newWaitEvent = new(false);
_thread = new Thread(Loop)
{
Name = $"Common.Nanosleep.{id}",
Priority = ThreadPriority.AboveNormal,
IsBackground = true
};
_thread.Start();
}
/// <summary>
/// Service requests to perform a nanosleep, signal parent pool when complete.
/// </summary>
private void Loop()
{
_newWaitEvent.WaitOne();
while (_running)
{
Nanosleep.Sleep(_nanoseconds);
_parent.Signal(this);
_newWaitEvent.WaitOne();
}
_newWaitEvent.Dispose();
}
/// <summary>
/// Assign a nanosleep for this thread to perform, then signal at the end.
/// </summary>
/// <param name="nanoseconds">Nanoseconds to sleep</param>
/// <param name="signalId">Signal ID</param>
/// <param name="timePoint">Target timepoint</param>
public void SleepAndSignal(long nanoseconds, long signalId, long timePoint)
{
_signalId = signalId;
_nanoseconds = nanoseconds;
_timePoint = timePoint;
_newWaitEvent.Set();
}
/// <summary>
/// Resurrect an active nanosleep's signal if its target timepoint is a close enough match.
/// </summary>
/// <param name="signalId">New signal id to assign the nanosleep</param>
/// <param name="timePoint">Target timepoint</param>
/// <returns>True if resurrected, false otherwise</returns>
public bool Resurrect(long signalId, long timePoint)
{
if (Math.Abs(timePoint - _timePoint) < _timePointEpsilon)
{
_signalId = signalId;
return true;
}
return false;
}
/// <summary>
/// Dispose the NanosleepThread, interrupting its worker loop.
/// </summary>
public void Dispose()
{
if (_running)
{
_running = false;
_newWaitEvent.Set();
}
}
}
private readonly object _lock = new();
private readonly List<NanosleepThread> _threads = new();
private readonly List<NanosleepThread> _active = new();
private readonly Stack<NanosleepThread> _free = new();
private readonly AutoResetEvent _signalTarget;
private long _signalId;
/// <summary>
/// Creates a new NanosleepPool with a target event to signal when a nanosleep completes.
/// </summary>
/// <param name="signalTarget">Event to signal when nanosleeps complete</param>
public NanosleepPool(AutoResetEvent signalTarget)
{
_signalTarget = signalTarget;
}
/// <summary>
/// Signal the target event (if the source sleep has not been superseded)
/// and free the nanosleep thread.
/// </summary>
/// <param name="thread">Nanosleep thread that completed</param>
private void Signal(NanosleepThread thread)
{
lock (_lock)
{
_active.Remove(thread);
_free.Push(thread);
if (thread.SignalId == _signalId)
{
_signalTarget.Set();
}
}
}
/// <summary>
/// Sleep for the given number of nanoseconds and signal the target event.
/// This does not block the caller thread.
/// </summary>
/// <param name="nanoseconds">Nanoseconds to sleep</param>
/// <param name="timePoint">Target timepoint</param>
/// <returns>True if the signal will be set, false otherwise</returns>
public bool SleepAndSignal(long nanoseconds, long timePoint)
{
lock (_lock)
{
_signalId++;
// Check active sleeps, if any line up with the requested timepoint then resurrect that nanosleep.
foreach (NanosleepThread existing in _active)
{
if (existing.Resurrect(_signalId, timePoint))
{
return true;
}
}
if (!_free.TryPop(out NanosleepThread thread))
{
if (_threads.Count >= MaxThreads)
{
return false;
}
thread = new NanosleepThread(this, _threads.Count);
_threads.Add(thread);
}
_active.Add(thread);
thread.SleepAndSignal(nanoseconds, _signalId, timePoint);
return true;
}
}
/// <summary>
/// Ignore the latest nanosleep.
/// </summary>
public void IgnoreSignal()
{
_signalId++;
}
/// <summary>
/// Dispose the NanosleepPool, disposing all of its active threads.
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
foreach (NanosleepThread thread in _threads)
{
thread.Dispose();
}
_threads.Clear();
}
}
}

View File

@@ -0,0 +1,104 @@
using Ryujinx.Common.SystemInterop;
using System;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
public static class PreciseSleepHelper
{
/// <summary>
/// Create a precise sleep event for the current platform.
/// </summary>
/// <returns>A precise sleep event</returns>
public static IPreciseSleepEvent CreateEvent()
{
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
{
return new NanosleepEvent();
}
else if (OperatingSystem.IsWindows())
{
return new WindowsSleepEvent();
}
else
{
return new SleepEvent();
}
}
/// <summary>
/// Sleeps up to the closest point to the timepoint that the OS reasonably allows.
/// The provided event is used by the timer to wake the current thread, and should not be signalled from any other source.
/// </summary>
/// <param name="evt">Event used to wake this thread</param>
/// <param name="timePoint">Target timepoint in host ticks</param>
public static void SleepUntilTimePoint(EventWaitHandle evt, long timePoint)
{
if (OperatingSystem.IsWindows())
{
WindowsGranularTimer.Instance.SleepUntilTimePointWithoutExternalSignal(evt, timePoint);
}
else
{
// Events might oversleep by a little, depending on OS.
// We don't want to miss the timepoint, so bias the wait to be lower.
// Nanosleep can possibly handle it better, too.
long accuracyBias = PerformanceCounter.TicksPerMillisecond / 2;
long now = PerformanceCounter.ElapsedTicks + accuracyBias;
long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
if (ms > 0)
{
evt.WaitOne((int)ms);
}
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsAndroid())
{
// Do a nanosleep.
now = PerformanceCounter.ElapsedTicks;
long ns = ((timePoint - now) * 1_000_000) / PerformanceCounter.TicksPerMillisecond;
Nanosleep.SleepAtMost(ns);
}
}
}
/// <summary>
/// Spinwait until the given timepoint. If wakeSignal is or becomes 1, return early.
/// Thread is allowed to yield.
/// </summary>
/// <param name="timePoint">Target timepoint in host ticks</param>
/// <param name="wakeSignal">Returns early if this is set to 1</param>
public static void SpinWaitUntilTimePoint(long timePoint, ref long wakeSignal)
{
SpinWait spinWait = new();
while (Interlocked.Read(ref wakeSignal) != 1 && PerformanceCounter.ElapsedTicks < timePoint)
{
// Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
if (spinWait.NextSpinWillYield)
{
Thread.Yield();
spinWait.Reset();
}
else
{
spinWait.SpinOnce();
}
}
}
/// <summary>
/// Spinwait until the given timepoint, with no opportunity to wake early.
/// </summary>
/// <param name="timePoint">Target timepoint in host ticks</param>
public static void SpinWaitUntilTimePoint(long timePoint)
{
while (PerformanceCounter.ElapsedTicks < timePoint)
{
Thread.SpinWait(5);
}
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// A cross-platform precise sleep event that has millisecond granularity.
/// </summary>
internal class SleepEvent : IPreciseSleepEvent
{
private readonly AutoResetEvent _waitEvent = new(false);
public long AdjustTimePoint(long timePoint, long timeoutNs)
{
// No adjustment
return timePoint;
}
public bool SleepUntil(long timePoint)
{
long now = PerformanceCounter.ElapsedTicks;
long ms = Math.Min((timePoint - now) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
if (ms > 0)
{
_waitEvent.WaitOne((int)ms);
return true;
}
return false;
}
public void Sleep()
{
_waitEvent.WaitOne();
}
public void Signal()
{
_waitEvent.Set();
}
public void Dispose()
{
GC.SuppressFinalize(this);
_waitEvent.Dispose();
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Common.SystemInterop
{
/// <summary>
/// Timer that attempts to align with the hardware timer interrupt,
/// and can alert listeners on ticks.
/// </summary>
[SupportedOSPlatform("windows")]
internal partial class WindowsGranularTimer
{
private const int MinimumGranularity = 5000;
private static readonly WindowsGranularTimer _instance = new();
public static WindowsGranularTimer Instance => _instance;
private readonly struct WaitingObject
{
public readonly long Id;
public readonly EventWaitHandle Signal;
public readonly long TimePoint;
public WaitingObject(long id, EventWaitHandle signal, long timePoint)
{
Id = id;
Signal = signal;
TimePoint = timePoint;
}
}
[LibraryImport("ntdll.dll", SetLastError = true)]
private static partial int NtSetTimerResolution(int DesiredResolution, [MarshalAs(UnmanagedType.Bool)] bool SetResolution, out int CurrentResolution);
[LibraryImport("ntdll.dll", SetLastError = true)]
private static partial int NtQueryTimerResolution(out int MaximumResolution, out int MinimumResolution, out int CurrentResolution);
[LibraryImport("ntdll.dll", SetLastError = true)]
private static partial uint NtDelayExecution([MarshalAs(UnmanagedType.Bool)] bool Alertable, ref long DelayInterval);
public long GranularityNs => _granularityNs;
public long GranularityTicks => _granularityTicks;
private readonly Thread _timerThread;
private long _granularityNs = MinimumGranularity * 100L;
private long _granularityTicks;
private long _lastTicks = PerformanceCounter.ElapsedTicks;
private long _lastId;
private readonly object _lock = new();
private readonly List<WaitingObject> _waitingObjects = new();
private WindowsGranularTimer()
{
_timerThread = new Thread(Loop)
{
IsBackground = true,
Name = "Common.WindowsTimer",
Priority = ThreadPriority.Highest
};
_timerThread.Start();
}
/// <summary>
/// Measure and initialize the timer's target granularity.
/// </summary>
private void Initialize()
{
NtQueryTimerResolution(out _, out int min, out int curr);
if (min > 0)
{
min = Math.Max(min, MinimumGranularity);
_granularityNs = min * 100L;
NtSetTimerResolution(min, true, out _);
}
else
{
_granularityNs = curr * 100L;
}
_granularityTicks = (_granularityNs * PerformanceCounter.TicksPerMillisecond) / 1_000_000;
}
/// <summary>
/// Main loop for the timer thread. Wakes every clock tick and signals any listeners,
/// as well as keeping track of clock alignment.
/// </summary>
private void Loop()
{
Initialize();
while (true)
{
long delayInterval = -1; // Next tick
NtSetTimerResolution((int)(_granularityNs / 100), true, out _);
NtDelayExecution(false, ref delayInterval);
long newTicks = PerformanceCounter.ElapsedTicks;
long nextTicks = newTicks + _granularityTicks;
lock (_lock)
{
for (int i = 0; i < _waitingObjects.Count; i++)
{
if (nextTicks > _waitingObjects[i].TimePoint)
{
// The next clock tick will be after the timepoint, we need to signal now.
_waitingObjects[i].Signal.Set();
_waitingObjects.RemoveAt(i--);
}
}
_lastTicks = newTicks;
}
}
}
/// <summary>
/// Sleep until a timepoint.
/// </summary>
/// <param name="evt">Reset event to use to be awoken by the clock tick, or an external signal</param>
/// <param name="timePoint">Target timepoint</param>
/// <returns>True if waited or signalled, false otherwise</returns>
public bool SleepUntilTimePoint(AutoResetEvent evt, long timePoint)
{
if (evt.WaitOne(0))
{
return true;
}
long id;
lock (_lock)
{
// Return immediately if the next tick is after the requested timepoint.
long nextTicks = _lastTicks + _granularityTicks;
if (nextTicks > timePoint)
{
return false;
}
id = ++_lastId;
_waitingObjects.Add(new WaitingObject(id, evt, timePoint));
}
evt.WaitOne();
lock (_lock)
{
for (int i = 0; i < _waitingObjects.Count; i++)
{
if (id == _waitingObjects[i].Id)
{
_waitingObjects.RemoveAt(i--);
break;
}
}
}
return true;
}
/// <summary>
/// Sleep until a timepoint, but don't expect any external signals.
/// </summary>
/// <remarks>
/// Saves some effort compared to the sleep that expects to be signalled.
/// </remarks>
/// <param name="evt">Reset event to use to be awoken by the clock tick</param>
/// <param name="timePoint">Target timepoint</param>
/// <returns>True if waited, false otherwise</returns>
public bool SleepUntilTimePointWithoutExternalSignal(EventWaitHandle evt, long timePoint)
{
long id;
lock (_lock)
{
// Return immediately if the next tick is after the requested timepoint.
long nextTicks = _lastTicks + _granularityTicks;
if (nextTicks > timePoint)
{
return false;
}
id = ++_lastId;
_waitingObjects.Add(new WaitingObject(id, evt, timePoint));
}
evt.WaitOne();
return true;
}
/// <summary>
/// Returns the two nearest clock ticks for a given timepoint.
/// </summary>
/// <param name="timePoint">Target timepoint</param>
/// <returns>The nearest clock ticks before and after the given timepoint</returns>
public (long, long) ReturnNearestTicks(long timePoint)
{
long last = _lastTicks;
long delta = timePoint - last;
long lowTicks = delta / _granularityTicks;
long highTicks = (delta + _granularityTicks - 1) / _granularityTicks;
return (last + lowTicks * _granularityTicks, last + highTicks * _granularityTicks);
}
}
}

View File

@@ -0,0 +1,92 @@
using Ryujinx.Common.SystemInterop;
using System;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
/// <summary>
/// A precise sleep event that uses Windows specific methods to increase clock resolution beyond 1ms,
/// use the clock's phase for more precise waits, and potentially align timepoints with it.
/// </summary>
[SupportedOSPlatform("windows")]
internal class WindowsSleepEvent : IPreciseSleepEvent
{
/// <summary>
/// The clock can drift a bit, so add this to encourage the clock to still wait if the next tick is forecasted slightly before it.
/// </summary>
private const long ErrorBias = 50000;
/// <summary>
/// Allowed to be 0.05ms away from the clock granularity to reduce precision.
/// </summary>
private const long ClockAlignedBias = 50000;
/// <summary>
/// The fraction of clock granularity above the timepoint that will align it down to the lower timepoint.
/// Currently set to the lower 1/4, so for 0.5ms granularity: 0.1ms would be rounded down, 0.2 ms would be rounded up.
/// </summary>
private const long ReverseTimePointFraction = 4;
private readonly AutoResetEvent _waitEvent = new(false);
private readonly WindowsGranularTimer _timer = WindowsGranularTimer.Instance;
/// <summary>
/// Set to true to disable timepoint realignment.
/// </summary>
public bool Precise { get; set; } = false;
public long AdjustTimePoint(long timePoint, long timeoutNs)
{
if (Precise || timePoint == long.MaxValue)
{
return timePoint;
}
// Does the timeout align with the host clock?
long granularity = _timer.GranularityNs;
long misalignment = timeoutNs % granularity;
if ((misalignment < ClockAlignedBias || misalignment > granularity - ClockAlignedBias) && timeoutNs > ClockAlignedBias)
{
// Inaccurate sleep for 0.5ms increments, typically.
(long low, long high) = _timer.ReturnNearestTicks(timePoint);
if (timePoint - low < _timer.GranularityTicks / ReverseTimePointFraction)
{
timePoint = low;
}
else
{
timePoint = high;
}
}
return timePoint;
}
public bool SleepUntil(long timePoint)
{
return _timer.SleepUntilTimePoint(_waitEvent, timePoint + (ErrorBias * PerformanceCounter.TicksPerMillisecond) / 1_000_000);
}
public void Sleep()
{
_waitEvent.WaitOne();
}
public void Signal()
{
_waitEvent.Set();
}
public void Dispose()
{
GC.SuppressFinalize(this);
_waitEvent.Dispose();
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -211,6 +211,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
int xCount = (int)_state.State.LineLengthIn;
int yCount = (int)_state.State.LineCount;
_channel.TextureManager.RefreshModifiedTextures();
_3dEngine.CreatePendingSyncs();
_3dEngine.FlushUboDirty();
@@ -279,7 +280,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
bool completeSource = IsTextureCopyComplete(src, srcLinear, srcBpp, srcStride, xCount, yCount);
bool completeDest = IsTextureCopyComplete(dst, dstLinear, dstBpp, dstStride, xCount, yCount);
if (completeSource && completeDest)
// Try to set the texture data directly,
// but only if we are doing a complete copy,
// and not for block linear to linear copies, since those are typically accessed from the CPU.
if (completeSource && completeDest && !(dstLinear && !srcLinear))
{
var target = memoryManager.Physical.TextureCache.FindTexture(
memoryManager,

View File

@@ -651,9 +651,35 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>True if the format is valid, false otherwise</returns>
public static bool TryGetTextureFormat(uint encoded, bool isSrgb, out FormatInfo format)
{
encoded |= (isSrgb ? 1u << 19 : 0u);
bool isPacked = (encoded & 0x80000000u) != 0;
if (isPacked)
{
encoded &= ~0x80000000u;
}
return _textureFormats.TryGetValue((TextureFormat)encoded, out format);
encoded |= isSrgb ? 1u << 19 : 0u;
bool found = _textureFormats.TryGetValue((TextureFormat)encoded, out format);
if (found && isPacked && !format.Format.IsDepthOrStencil())
{
// If the packed flag is set, then the components of the pixel are tightly packed into the
// GPU registers on the shader.
// We can get the same behaviour by aliasing the texture as a format with the same amount of
// bytes per pixel, but only a single or the lowest possible number of components.
format = format.BytesPerPixel switch
{
1 => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1),
2 => new FormatInfo(Format.R16Unorm, 1, 1, 2, 1),
4 => new FormatInfo(Format.R32Float, 1, 1, 4, 1),
8 => new FormatInfo(Format.R32G32Float, 1, 1, 8, 2),
16 => new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4),
_ => format,
};
}
return found;
}
/// <summary>

View File

@@ -101,11 +101,6 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public bool AlwaysFlushOnOverlap { get; private set; }
/// <summary>
/// Indicates that the texture was modified since the last time it was flushed.
/// </summary>
public bool ModifiedSinceLastFlush { get; set; }
/// <summary>
/// Increments when the host texture is swapped, or when the texture is removed from all pools.
/// </summary>
@@ -1443,7 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer)
{
_modifiedStale = false;
Group.SignalModifying(this, bound, bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer);
Group.SignalModifying(this, bound);
}
_physicalMemory.TextureCache.Lift(this);

View File

@@ -2,6 +2,8 @@ using Ryujinx.Common;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Texture;
using System;
using System.Diagnostics;
using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Image
{
@@ -339,7 +341,20 @@ namespace Ryujinx.Graphics.Gpu.Image
if (lhs.FormatInfo.BytesPerPixel != rhs.FormatInfo.BytesPerPixel && IsIncompatibleFormatAliasingAllowed(lhs.FormatInfo, rhs.FormatInfo))
{
alignedWidthMatches = lhsSize.Width * lhs.FormatInfo.BytesPerPixel == rhsSize.Width * rhs.FormatInfo.BytesPerPixel;
// If the formats are incompatible, but the texture strides match,
// we might allow them to be copy compatible depending on the format.
// The strides are aligned because the format with higher bytes per pixel
// might need a bit of padding at the end due to one width not being a multiple of the other.
Debug.Assert((1 << BitOperations.Log2((uint)lhs.FormatInfo.BytesPerPixel)) == lhs.FormatInfo.BytesPerPixel);
Debug.Assert((1 << BitOperations.Log2((uint)rhs.FormatInfo.BytesPerPixel)) == rhs.FormatInfo.BytesPerPixel);
int alignment = Math.Max(lhs.FormatInfo.BytesPerPixel, rhs.FormatInfo.BytesPerPixel);
int lhsStride = BitUtils.AlignUp(lhsSize.Width * lhs.FormatInfo.BytesPerPixel, alignment);
int rhsStride = BitUtils.AlignUp(rhsSize.Width * rhs.FormatInfo.BytesPerPixel, alignment);
alignedWidthMatches = lhsStride == rhsStride;
}
TextureViewCompatibility result = TextureViewCompatibility.Full;
@@ -718,7 +733,8 @@ namespace Ryujinx.Graphics.Gpu.Image
(lhsFormat, rhsFormat) = (rhsFormat, lhsFormat);
}
return lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm;
return (lhsFormat.Format == Format.R8G8B8A8Unorm && rhsFormat.Format == Format.R32G32B32A32Float) ||
(lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm);
}
/// <summary>

View File

@@ -709,8 +709,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="texture">The texture that has been modified</param>
/// <param name="bound">True if this texture is being bound, false if unbound</param>
/// <param name="setModified">Indicates if the modified flag should be set</param>
public void SignalModifying(Texture texture, bool bound, bool setModified)
public void SignalModifying(Texture texture, bool bound)
{
ModifiedSequence = _context.GetModifiedSequence();
@@ -722,7 +721,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
TextureGroupHandle group = _handles[baseHandle + i];
group.SignalModifying(bound, _context, setModified);
group.SignalModifying(bound, _context);
}
});
}
@@ -993,26 +992,6 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// The action to perform when a memory tracking handle is flipped to dirty.
/// This notifies overlapping textures that the memory needs to be synchronized.
/// </summary>
/// <param name="groupHandle">The handle that a dirty flag was set on</param>
private void DirtyAction(TextureGroupHandle groupHandle)
{
// Notify all textures that belong to this handle.
Storage.SignalGroupDirty();
lock (groupHandle.Overlaps)
{
foreach (Texture overlap in groupHandle.Overlaps)
{
overlap.SignalGroupDirty();
}
}
}
/// <summary>
/// Generate a CpuRegionHandle for a given address and size range in CPU VA.
/// </summary>
@@ -1084,11 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Image
views,
result.ToArray());
foreach (RegionHandle handle in result)
{
handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
}
return groupHandle;
}
@@ -1360,11 +1334,6 @@ namespace Ryujinx.Graphics.Gpu.Image
var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles);
foreach (RegionHandle handle in cpuRegionHandles)
{
handle.RegisterDirtyEvent(() => DirtyAction(groupHandle));
}
handles = new TextureGroupHandle[] { groupHandle };
}
else
@@ -1620,6 +1589,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified)
{
handle.Modified = false;
handle.DeferredCopy = null;
Storage.SignalModifiedDirty();
lock (handle.Overlaps)
@@ -1666,8 +1636,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
Storage.ModifiedSinceLastFlush = false;
// 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.

View File

@@ -152,6 +152,32 @@ namespace Ryujinx.Graphics.Gpu.Image
// Linear textures are presumed to be used for readback initially.
_flushBalance = FlushBalanceThreshold + FlushBalanceIncrement;
}
foreach (RegionHandle handle in handles)
{
handle.RegisterDirtyEvent(DirtyAction);
}
}
/// <summary>
/// The action to perform when a memory tracking handle is flipped to dirty.
/// This notifies overlapping textures that the memory needs to be synchronized.
/// </summary>
private void DirtyAction()
{
// Notify all textures that belong to this handle.
_group.Storage.SignalGroupDirty();
lock (Overlaps)
{
foreach (Texture overlap in Overlaps)
{
overlap.SignalGroupDirty();
}
}
DeferredCopy = null;
}
/// <summary>
@@ -304,17 +330,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="bound">True if this handle is being bound, false if unbound</param>
/// <param name="context">The GPU context to register a sync action on</param>
/// <param name="setModified">Indicates if the modified flag should be set</param>
public void SignalModifying(bool bound, GpuContext context, bool setModified)
{
if (setModified)
public void SignalModifying(bool bound, GpuContext context)
{
SignalModified(context);
}
else
{
RegisterSync(context);
}
if (!bound && _syncActionRegistered && NextSyncCopies())
{
@@ -457,7 +475,6 @@ namespace Ryujinx.Graphics.Gpu.Image
public void DeferCopy(TextureGroupHandle copyFrom)
{
Modified = false;
DeferredCopy = copyFrom;
_group.Storage.SignalGroupDirty();
@@ -514,7 +531,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
existing.Other.Handle.CreateCopyDependency(this);
if (copyToOther)
if (copyToOther && Modified)
{
existing.Other.Handle.DeferCopy(this);
}
@@ -558,10 +575,10 @@ namespace Ryujinx.Graphics.Gpu.Image
if (fromHandle != null)
{
// Only copy if the copy texture is still modified.
// It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush.
// DeferredCopy will be set to null if new data is written from CPU (see the DirtyAction method).
// It will also set as unmodified if a copy is deferred to it.
shouldCopy = fromHandle.Modified;
shouldCopy = true;
if (fromHandle._bindCount == 0)
{

View File

@@ -20,8 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly Texture[] _rtColors;
private readonly ITexture[] _rtHostColors;
private readonly bool[] _rtColorsBound;
private Texture _rtDepthStencil;
private ITexture _rtHostDs;
private bool _rtDsBound;
public int ClipRegionWidth { get; private set; }
public int ClipRegionHeight { get; private set; }
@@ -51,6 +53,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_rtColors = new Texture[Constants.TotalRenderTargets];
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
_rtColorsBound = new bool[Constants.TotalRenderTargets];
}
/// <summary>
@@ -153,8 +156,15 @@ namespace Ryujinx.Graphics.Gpu.Image
bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor);
if (_rtColors[index] != color)
{
if (_rtColorsBound[index])
{
_rtColors[index]?.SignalModifying(false);
}
else
{
_rtColorsBound[index] = true;
}
if (color != null)
{
@@ -179,8 +189,15 @@ namespace Ryujinx.Graphics.Gpu.Image
bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor);
if (_rtDepthStencil != depthStencil)
{
if (_rtDsBound)
{
_rtDepthStencil?.SignalModifying(false);
}
else
{
_rtDsBound = true;
}
if (depthStencil != null)
{
@@ -419,7 +436,12 @@ namespace Ryujinx.Graphics.Gpu.Image
if (dsTexture != null)
{
hostDsTexture = dsTexture.HostTexture;
dsTexture.ModifiedSinceLastFlush = true;
if (!_rtDsBound)
{
dsTexture.SignalModifying(true);
_rtDsBound = true;
}
}
if (_rtHostDs != hostDsTexture)
@@ -436,7 +458,12 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture != null)
{
hostTexture = texture.HostTexture;
texture.ModifiedSinceLastFlush = true;
if (!_rtColorsBound[index])
{
texture.SignalModifying(true);
_rtColorsBound[index] = true;
}
}
if (_rtHostColors[index] != hostTexture)
@@ -466,6 +493,31 @@ namespace Ryujinx.Graphics.Gpu.Image
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
}
/// <summary>
/// Marks all currently bound render target textures as modified, and also makes them be set as modified again on next use.
/// </summary>
public void RefreshModifiedTextures()
{
Texture dsTexture = _rtDepthStencil;
if (dsTexture != null && _rtDsBound)
{
dsTexture.SignalModifying(false);
_rtDsBound = false;
}
for (int index = 0; index < _rtColors.Length; index++)
{
Texture texture = _rtColors[index];
if (texture != null && _rtColorsBound[index])
{
texture.SignalModifying(false);
_rtColorsBound[index] = false;
}
}
}
/// <summary>
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
/// </summary>
@@ -501,12 +553,20 @@ namespace Ryujinx.Graphics.Gpu.Image
_samplerPoolCache.Dispose();
for (int i = 0; i < _rtColors.Length; i++)
{
if (_rtColorsBound[i])
{
_rtColors[i]?.DecrementReferenceCount();
}
_rtColors[i] = null;
}
if (_rtDsBound)
{
_rtDepthStencil?.DecrementReferenceCount();
}
_rtDepthStencil = null;
}
}

View File

@@ -430,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo))
{
if (gpuVa != 0 && (int)format > 0)
if (gpuVa != 0 && format != 0)
{
Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb}).");
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -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 = 5791;
private const uint CodeGenVersion = 5958;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";

View File

@@ -37,10 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// <returns>The incremented value of the syncpoint</returns>
public uint IncrementSyncpoint(uint id)
{
if (id >= MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].Increment();
}
@@ -53,10 +50,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// <returns>The value of the syncpoint</returns>
public uint GetSyncpointValue(uint id)
{
if (id >= MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].Value;
}
@@ -72,10 +66,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// <returns>The created SyncpointWaiterHandle object or null if already past threshold</returns>
public SyncpointWaiterHandle RegisterCallbackOnSyncpoint(uint id, uint threshold, Action<SyncpointWaiterHandle> callback)
{
if (id >= MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
return _syncpoints[id].RegisterCallback(threshold, callback);
}
@@ -88,10 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// <exception cref="System.ArgumentOutOfRangeException">Thrown when id >= MaxHardwareSyncpoints</exception>
public void UnregisterCallback(uint id, SyncpointWaiterHandle waiterInformation)
{
if (id >= MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
_syncpoints[id].UnregisterCallback(waiterInformation);
}
@@ -107,10 +95,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization
/// <returns>True if timed out</returns>
public bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout)
{
if (id >= MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints);
// TODO: Remove this when GPU channel scheduling will be implemented.
if (timeout == Timeout.InfiniteTimeSpan)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -69,10 +69,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
public Operand GetDest(int index)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0);
return _dest;
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -29,7 +29,16 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery())
{
Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block);
Operand bindlessHandle = texOp.GetSource(0);
// In some cases the compiler uses a shuffle operation to get the handle,
// for some textureGrad implementations. In those cases, we can skip the shuffle.
if (bindlessHandle.AsgOp is Operation shuffleOp && shuffleOp.Inst == Instruction.Shuffle)
{
bindlessHandle = shuffleOp.GetSource(0);
}
bindlessHandle = Utils.FindLastOperation(bindlessHandle, block);
// Some instructions do not encode an accurate sampler type:
// - Most instructions uses the same type for 1D and Buffer.
@@ -55,7 +64,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
continue;
}
if (bindlessHandle.AsgOp is not Operation handleCombineOp)
if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp))
{
continue;
}
@@ -199,9 +208,64 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
}
}
private static bool TryGetOperation(INode asgOp, out Operation outOperation)
{
if (asgOp is PhiNode phi)
{
// If we have a phi, let's check if all inputs are effectively the same value.
// If so, we can "see through" the phi and pick any of the inputs (since they are all the same).
Operand firstSrc = phi.GetSource(0);
for (int index = 1; index < phi.SourcesCount; index++)
{
if (!IsSameOperand(firstSrc, phi.GetSource(index)))
{
outOperation = null;
return false;
}
}
asgOp = firstSrc.AsgOp;
}
if (asgOp is Operation operation)
{
outOperation = operation;
return true;
}
outOperation = null;
return false;
}
private static bool IsSameOperand(Operand x, Operand y)
{
if (x.Type == y.Type && x.Type == OperandType.LocalVariable)
{
return x.AsgOp is Operation xOp &&
y.AsgOp is Operation yOp &&
xOp.Inst == Instruction.BitwiseOr &&
yOp.Inst == Instruction.BitwiseOr &&
AreBothEqualConstantBuffers(xOp.GetSource(0), yOp.GetSource(0)) &&
AreBothEqualConstantBuffers(xOp.GetSource(1), yOp.GetSource(1));
}
return false;
}
private static bool AreBothEqualConstantBuffers(Operand x, Operand y)
{
return x.Type == y.Type && x.Value == y.Value && x.Type == OperandType.ConstantBuffer;
}
private static Operand GetSourceForMaskedHandle(Operation asgOp, uint mask)
{
// Assume it was already checked that the operation is bitwise AND.
Operand src0 = asgOp.GetSource(0);
Operand src1 = asgOp.GetSource(1);
@@ -210,6 +274,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
// We can't check if the mask matches here as both operands are from a constant buffer.
// Be optimistic and assume it matches. Avoid constant buffer 1 as official drivers
// uses this one to store compiler constants.
return src0.GetCbufSlot() == 1 ? src1 : src0;
}
else if (src0.Type == OperandType.ConstantBuffer && src1.Type == OperandType.Constant)

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
@@ -102,11 +103,11 @@ namespace Ryujinx.Graphics.Texture.Utils
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaColor32 operator <<(RgbaColor32 x, int shift)
public static RgbaColor32 operator <<(RgbaColor32 x, [ConstantExpected] byte shift)
{
if (Sse2.IsSupported)
{
return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, (byte)shift));
return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, shift));
}
else
{
@@ -115,11 +116,11 @@ namespace Ryujinx.Graphics.Texture.Utils
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RgbaColor32 operator >>(RgbaColor32 x, int shift)
public static RgbaColor32 operator >>(RgbaColor32 x, [ConstantExpected] byte shift)
{
if (Sse2.IsSupported)
{
return new RgbaColor32(Sse2.ShiftRightLogical(x._color, (byte)shift));
return new RgbaColor32(Sse2.ShiftRightLogical(x._color, shift));
}
else
{

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -189,7 +189,7 @@ namespace Ryujinx.Graphics.Vulkan
PipelineStageFlags.AllCommandsBit,
0,
1,
new ReadOnlySpan<MemoryBarrier>(memoryBarrier),
new ReadOnlySpan<MemoryBarrier>(in memoryBarrier),
0,
ReadOnlySpan<BufferMemoryBarrier>.Empty,
0,

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -33,9 +33,5 @@ namespace Ryujinx.Graphics.Vulkan
public VulkanException(string message, Exception innerException) : base(message, innerException)
{
}
protected VulkanException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -35,8 +35,6 @@ namespace Ryujinx.HLE.Exceptions
Request = context.Request;
}
protected ServiceNotImplementedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
public override string Message
{
get

View File

@@ -419,11 +419,8 @@ namespace Ryujinx.HLE.FileSystem
}
if (locationList != null)
{
if (locationList.Contains(entry))
{
locationList.Remove(entry);
}
locationList.AddLast(entry);
}

View File

@@ -1,4 +1,5 @@
using Ryujinx.Common;
using Ryujinx.Common.PreciseSleep;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -23,7 +24,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
private readonly KernelContext _context;
private readonly List<WaitingObject> _waitingObjects;
private AutoResetEvent _waitEvent;
private IPreciseSleepEvent _waitEvent;
private bool _keepRunning;
private long _enforceWakeupFromSpinWait;
@@ -54,6 +55,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
timePoint = long.MaxValue;
}
timePoint = _waitEvent.AdjustTimePoint(timePoint, timeout);
lock (_context.CriticalSection.Lock)
{
_waitingObjects.Add(new WaitingObject(schedulerObj, timePoint));
@@ -64,7 +67,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
}
}
_waitEvent.Set();
_waitEvent.Signal();
}
public void UnscheduleFutureInvocation(IKFutureSchedulerObject schedulerObj)
@@ -83,10 +86,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
private void WaitAndCheckScheduledObjects()
{
SpinWait spinWait = new();
WaitingObject next;
using (_waitEvent = new AutoResetEvent(false))
using (_waitEvent = PreciseSleepHelper.CreateEvent())
{
while (_keepRunning)
{
@@ -103,30 +105,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
if (next.TimePoint > timePoint)
{
long ms = Math.Min((next.TimePoint - timePoint) / PerformanceCounter.TicksPerMillisecond, int.MaxValue);
if (ms > 0)
if (!_waitEvent.SleepUntil(next.TimePoint))
{
_waitEvent.WaitOne((int)ms);
}
else
{
while (Interlocked.Read(ref _enforceWakeupFromSpinWait) != 1 && PerformanceCounter.ElapsedTicks < next.TimePoint)
{
// Our time is close - don't let SpinWait go off and potentially Thread.Sleep().
if (spinWait.NextSpinWillYield)
{
Thread.Yield();
spinWait.Reset();
}
else
{
spinWait.SpinOnce();
}
}
spinWait.Reset();
PreciseSleepHelper.SpinWaitUntilTimePoint(next.TimePoint, ref _enforceWakeupFromSpinWait);
}
}
@@ -145,7 +126,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
}
else
{
_waitEvent.WaitOne();
_waitEvent.Sleep();
}
}
}
@@ -212,7 +193,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
public void Dispose()
{
_keepRunning = false;
_waitEvent?.Set();
_waitEvent?.Signal();
}
}
}

View File

@@ -85,10 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl
public void SetSyncpointMinEqualSyncpointMax(uint id)
{
if (id >= SynchronizationManager.MaxHardwareSyncpoints)
{
throw new ArgumentOutOfRangeException(nameof(id));
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)SynchronizationManager.MaxHardwareSyncpoints);
int value = (int)ReadSyncpointValue(id);

View File

@@ -27,7 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
int controlLength = message.Control == null ? 0 : message.Control.Length;
BsdSocketFlags flags = message.Flags;
if (!MemoryMarshal.TryWrite(rawData, ref msgNameLength))
if (!MemoryMarshal.TryWrite(rawData, in msgNameLength))
{
return LinuxError.EFAULT;
}
@@ -45,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
rawData = rawData[msgNameLength..];
}
if (!MemoryMarshal.TryWrite(rawData, ref iovCount))
if (!MemoryMarshal.TryWrite(rawData, in iovCount))
{
return LinuxError.EFAULT;
}
@@ -58,7 +58,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
{
ulong iovLength = (ulong)message.Iov[index].Length;
if (!MemoryMarshal.TryWrite(rawData, ref iovLength))
if (!MemoryMarshal.TryWrite(rawData, in iovLength))
{
return LinuxError.EFAULT;
}
@@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
}
}
if (!MemoryMarshal.TryWrite(rawData, ref controlLength))
if (!MemoryMarshal.TryWrite(rawData, in controlLength))
{
return LinuxError.EFAULT;
}
@@ -96,14 +96,14 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types
rawData = rawData[controlLength..];
}
if (!MemoryMarshal.TryWrite(rawData, ref flags))
if (!MemoryMarshal.TryWrite(rawData, in flags))
{
return LinuxError.EFAULT;
}
rawData = rawData[sizeof(BsdSocketFlags)..];
if (!MemoryMarshal.TryWrite(rawData, ref message.Length))
if (!MemoryMarshal.TryWrite(rawData, in message.Length))
{
return LinuxError.EFAULT;
}

View File

@@ -654,7 +654,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres
}
uint sentinel = 0;
MemoryMarshal.Write(data, ref sentinel);
MemoryMarshal.Write(data, in sentinel);
data = data[sizeof(uint)..];
return region.Memory.Span.Length - data.Length;

View File

@@ -94,7 +94,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
Header.ToNetworkOrder();
MemoryMarshal.Write(buffer, ref Header);
MemoryMarshal.Write(buffer, in Header);
buffer = buffer[Unsafe.SizeOf<AddrInfoSerializedHeader>()..];
@@ -103,7 +103,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
AddrInfo4 socketAddress = SocketAddress.Value;
socketAddress.ToNetworkOrder();
MemoryMarshal.Write(buffer, ref socketAddress);
MemoryMarshal.Write(buffer, in socketAddress);
buffer = buffer[Unsafe.SizeOf<AddrInfo4>()..];
}
@@ -117,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types
Array4<byte> rawIPv4Address = RawIPv4Address.Value;
AddrInfo4.RawIpv4AddressNetworkEndianSwap(ref rawIPv4Address);
MemoryMarshal.Write(buffer, ref rawIPv4Address);
MemoryMarshal.Write(buffer, in rawIPv4Address);
buffer = buffer[Unsafe.SizeOf<Array4<byte>>()..];
}

View File

@@ -161,7 +161,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
}
else
{
throw exception;
throw;
}
}
finally
@@ -206,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
}
else
{
throw exception;
throw;
}
}
finally

View File

@@ -1,5 +1,7 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.PreciseSleep;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvMap;
@@ -23,9 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
private readonly Thread _composerThread;
private readonly Stopwatch _chrono;
private readonly ManualResetEvent _event = new(false);
private readonly AutoResetEvent _event = new(false);
private readonly AutoResetEvent _nextFrameEvent = new(true);
private long _ticks;
private long _ticksPerFrame;
@@ -64,11 +64,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
_composerThread = new Thread(HandleComposition)
{
Name = "SurfaceFlinger.Composer",
Priority = ThreadPriority.AboveNormal
};
_chrono = new Stopwatch();
_chrono.Start();
_ticks = 0;
_spinTicks = Stopwatch.Frequency / 500;
_1msTicks = Stopwatch.Frequency / 1000;
@@ -299,11 +297,11 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
_isRunning = true;
long lastTicks = _chrono.ElapsedTicks;
long lastTicks = PerformanceCounter.ElapsedTicks;
while (_isRunning)
{
long ticks = _chrono.ElapsedTicks;
long ticks = PerformanceCounter.ElapsedTicks;
if (_swapInterval == 0)
{
@@ -336,21 +334,16 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
}
// Sleep if possible. If the time til the next frame is too low, spin wait instead.
long diff = _ticksPerFrame - (_ticks + _chrono.ElapsedTicks - ticks);
long diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
if (diff > 0)
{
PreciseSleepHelper.SleepUntilTimePoint(_event, PerformanceCounter.ElapsedTicks + diff);
diff = _ticksPerFrame - (_ticks + PerformanceCounter.ElapsedTicks - ticks);
if (diff < _spinTicks)
{
do
{
// SpinWait is a little more HT/SMT friendly than aggressively updating/checking ticks.
// The value of 5 still gives us quite a bit of precision (~0.0003ms variance at worst) while waiting a reasonable amount of time.
Thread.SpinWait(5);
ticks = _chrono.ElapsedTicks;
_ticks += ticks - lastTicks;
lastTicks = ticks;
} while (_ticks < _ticksPerFrame);
PreciseSleepHelper.SpinWaitUntilTimePoint(PerformanceCounter.ElapsedTicks + diff);
}
else
{

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -52,10 +52,7 @@ namespace Ryujinx.Memory.Range
{
if (HasSingleRange)
{
if (_singleRange.Size - offset < size)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
ArgumentOutOfRangeException.ThrowIfGreaterThan(size, _singleRange.Size - offset);
return new MultiRange(_singleRange.Address + offset, size);
}
@@ -108,10 +105,7 @@ namespace Ryujinx.Memory.Range
{
if (HasSingleRange)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0);
return _singleRange;
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>

View File

@@ -1,9 +0,0 @@
namespace Ryujinx.Ui.Common.Configuration
{
public enum ConfigurationLoadResult
{
Success = 0,
NotLoaded = 1,
MigratedFromPreVulkan = 1 << 8,
}
}

View File

@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.Ui.Common.Configuration.System;
using Ryujinx.Ui.Common.Configuration.Ui;
using Ryujinx.Ui.Common.Helper;
@@ -763,7 +764,7 @@ namespace Ryujinx.Ui.Common.Configuration
Graphics.ResScaleCustom.Value = 1.0f;
Graphics.MaxAnisotropy.Value = -1.0f;
Graphics.AspectRatio.Value = AspectRatio.Fixed16x9;
Graphics.GraphicsBackend.Value = OperatingSystem.IsMacOS() ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGl;
Graphics.GraphicsBackend.Value = DefaultGraphicsBackend();
Graphics.PreferredGpu.Value = "";
Graphics.ShadersDumpPath.Value = "";
Logger.EnableDebug.Value = false;
@@ -907,7 +908,7 @@ namespace Ryujinx.Ui.Common.Configuration
};
}
public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath)
public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath)
{
bool configurationFileUpdated = false;
@@ -916,12 +917,8 @@ namespace Ryujinx.Ui.Common.Configuration
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default.");
LoadDefault();
return ConfigurationLoadResult.NotLoaded;
}
ConfigurationLoadResult result = ConfigurationLoadResult.Success;
if (configurationFileFormat.Version < 2)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2.");
@@ -1336,8 +1333,6 @@ namespace Ryujinx.Ui.Common.Configuration
configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl;
result |= ConfigurationLoadResult.MigratedFromPreVulkan;
configurationFileUpdated = true;
}
@@ -1535,8 +1530,18 @@ namespace Ryujinx.Ui.Common.Configuration
Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
}
}
return result;
private static GraphicsBackend DefaultGraphicsBackend()
{
// Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan.
// Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer.
if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0)
{
return GraphicsBackend.Vulkan;
}
return GraphicsBackend.OpenGl;
}
private static void LogValueChange<T>(ReactiveEventArgs<T> eventArgs, string valueName)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -62,6 +62,7 @@
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
</ItemGroup>
</Project>

View File

@@ -177,8 +177,6 @@ namespace Ryujinx
? appDataConfigurationPath
: null;
bool showVulkanPrompt = false;
if (ConfigurationPath == null)
{
// No configuration, we load the default values and save it to disk
@@ -186,26 +184,17 @@ namespace Ryujinx
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
showVulkanPrompt = true;
}
else
{
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationLoadResult result = ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
if ((result & ConfigurationLoadResult.MigratedFromPreVulkan) != 0)
{
showVulkanPrompt = true;
}
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
}
else
{
ConfigurationState.Instance.LoadDefault();
showVulkanPrompt = true;
Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
}
}
@@ -216,12 +205,10 @@ namespace Ryujinx
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
showVulkanPrompt = false;
}
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
showVulkanPrompt = false;
}
}
@@ -343,35 +330,6 @@ namespace Ryujinx
}, TaskContinuationOptions.OnlyOnFaulted);
}
if (showVulkanPrompt)
{
var buttonTexts = new Dictionary<int, string>()
{
{ 0, "Yes (Vulkan)" },
{ 1, "No (OpenGL)" },
};
ResponseType response = GtkDialog.CreateCustomDialog(
"Ryujinx - Default graphics backend",
"Use Vulkan as default graphics backend?",
"Ryujinx now supports the Vulkan API. " +
"Vulkan greatly improves shader compilation performance, " +
"and fixes some graphical glitches; however, since it is a new feature, " +
"you may experience some issues that did not occur with OpenGL.\n\n" +
"Note that you will also lose any existing shader cache the first time you start a game " +
"on version 1.1.200 onwards, because Vulkan required changes to the shader cache that makes it incompatible with previous versions.\n\n" +
"Would you like to set Vulkan as the default graphics backend? " +
"You can change this at any time on the settings window.",
buttonTexts,
MessageType.Question);
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = response == 0
? GraphicsBackend.Vulkan
: GraphicsBackend.OpenGl;
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
}
Application.Run();
}

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
@@ -25,7 +25,7 @@
<PackageReference Include="GtkSharp.Dependencies.osx" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
<PackageReference Include="OpenTK.Core" />
<PackageReference Include="OpenTK.Graphics" />
<PackageReference Include="SPB" />

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>