Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#.
This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds.
It was written from scratch and development on the project began in September 2017.
- Ryujinx is available on Github under the MIT license.
+ Ryujinx is available on GitHub under the MIT license.
@@ -63,7 +54,7 @@ failing to meet this requirement may result in a poor gameplay experience or une
## Latest build
-Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
+Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
@@ -91,7 +82,7 @@ If you are planning to contribute or just want to learn more about this project
It translates the ARM code to a custom IR, performs a few optimizations, and turns that into x86 code.
There are three memory manager options available depending on the user's preference, leveraging both software-based (slower) and host-mapped modes (much faster).
The fastest option (host, unchecked) is set by default.
- Ryujinx also features an optional Profiled Persistent Translation Cache, which essentially caches translated functions so that they do not need to be translated every time the game loads.
+ Ryujinx also features an optional Profiled Persistent Translation Cache, which essentially caches translated functions so that they do not need to be translated every time the game loads.
The net result is a significant reduction in load times (the amount of time between launching a game and arriving at the title screen) for nearly every game.
NOTE: This feature is enabled by default in the Options menu > System tab.
You must launch the game at least twice to the title screen or beyond before performance improvements are unlocked on the third launch!
diff --git a/Ryujinx.sln b/Ryujinx.sln
index d661b903c..71d5f6dd9 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -80,16 +80,22 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Metal", "src\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj", "{C08931FA-1191-417A-864F-3882D93E683B}"
+ ProjectSection(ProjectDependencies) = postProject
+ {A602AE97-91A5-4608-8DF1-EBF4ED7A0B9E} = {A602AE97-91A5-4608-8DF1-EBF4ED7A0B9E}
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ .github\workflows\build.yml = .github\workflows\build.yml
+ .github\workflows\canary.yml = .github\workflows\canary.yml
Directory.Packages.props = Directory.Packages.props
- .github/workflows/release.yml = .github/workflows/release.yml
- .github/workflows/canary.yml = .github/workflows/canary.yml
- .github/workflows/build.yml = .github/workflows/build.yml
+ .github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -252,6 +258,13 @@ Global
{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C08931FA-1191-417A-864F-3882D93E683B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C08931FA-1191-417A-864F-3882D93E683B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C08931FA-1191-417A-864F-3882D93E683B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C08931FA-1191-417A-864F-3882D93E683B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist
index 53929f95e..2602f9905 100644
--- a/distribution/macos/Info.plist
+++ b/distribution/macos/Info.plist
@@ -40,11 +40,11 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.1
+ 1.2CFBundleSignature????CFBundleVersion
- 1.1.0
+ 1.2.0NSHighResolutionCapableCSResourcesFileMapped
diff --git a/distribution/macos/Ryujinx.icns b/distribution/macos/Ryujinx.icns
index 1bb88a5d7..fd5cd7f7e 100644
Binary files a/distribution/macos/Ryujinx.icns and b/distribution/macos/Ryujinx.icns differ
diff --git a/distribution/macos/updater.sh b/distribution/macos/updater.sh
index 12e4c3aa1..0465d7c91 100755
--- a/distribution/macos/updater.sh
+++ b/distribution/macos/updater.sh
@@ -17,7 +17,7 @@ error_handler() {
set the button_pressed to the button returned of the result
if the button_pressed is \"Open Download Page\" then
- open location \"https://ryujinx.org/download\"
+ open location \"https://ryujinx.app/download\"
end if
"""
@@ -54,4 +54,4 @@ if [ "$#" -le 3 ]; then
open -a "$INSTALL_DIRECTORY"
else
open -a "$INSTALL_DIRECTORY" --args "${APP_ARGUMENTS[@]}"
-fi
\ No newline at end of file
+fi
diff --git a/distribution/misc/Logo.svg b/distribution/misc/Logo.svg
index d3327f2ef..00bba42f0 100644
--- a/distribution/misc/Logo.svg
+++ b/distribution/misc/Logo.svg
@@ -1 +1,4 @@
-
\ No newline at end of file
+
+
diff --git a/global.json b/global.json
index 391ba3c2a..cdbb589ed 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "9.0.100",
"rollForward": "latestFeature"
}
}
diff --git a/src/ARMeilleure/ARMeilleure.csproj b/src/ARMeilleure/ARMeilleure.csproj
index 4b67fdb3b..5b6c5a6da 100644
--- a/src/ARMeilleure/ARMeilleure.csproj
+++ b/src/ARMeilleure/ARMeilleure.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefaultItemExcludes);._*
diff --git a/src/ARMeilleure/Decoders/DecoderHelper.cs b/src/ARMeilleure/Decoders/DecoderHelper.cs
index 35e573955..c39a8a88b 100644
--- a/src/ARMeilleure/Decoders/DecoderHelper.cs
+++ b/src/ARMeilleure/Decoders/DecoderHelper.cs
@@ -1,4 +1,5 @@
using ARMeilleure.Common;
+using System;
namespace ARMeilleure.Decoders
{
@@ -149,7 +150,7 @@ namespace ARMeilleure.Decoders
return (((long)opCode << 45) >> 48) & ~3;
}
- public static bool VectorArgumentsInvalid(bool q, params int[] args)
+ public static bool VectorArgumentsInvalid(bool q, params ReadOnlySpan args)
{
if (q)
{
diff --git a/src/ARMeilleure/Instructions/SoftFallback.cs b/src/ARMeilleure/Instructions/SoftFallback.cs
index c4fe677bf..899326c4b 100644
--- a/src/ARMeilleure/Instructions/SoftFallback.cs
+++ b/src/ARMeilleure/Instructions/SoftFallback.cs
@@ -264,7 +264,7 @@ namespace ARMeilleure.Instructions
return TblOrTbx(dest, vector, bytes, tb0, tb1, tb2, tb3);
}
- private static V128 TblOrTbx(V128 dest, V128 vector, int bytes, params V128[] tb)
+ private static V128 TblOrTbx(V128 dest, V128 vector, int bytes, params ReadOnlySpan tb)
{
byte[] res = new byte[16];
diff --git a/src/ARMeilleure/IntermediateRepresentation/Operation.cs b/src/ARMeilleure/IntermediateRepresentation/Operation.cs
index b0dc173af..4bc3a2e09 100644
--- a/src/ARMeilleure/IntermediateRepresentation/Operation.cs
+++ b/src/ARMeilleure/IntermediateRepresentation/Operation.cs
@@ -337,7 +337,7 @@ namespace ARMeilleure.IntermediateRepresentation
return result;
}
- public static Operation Operation(Intrinsic intrin, Operand dest, params Operand[] srcs)
+ public static Operation Operation(Intrinsic intrin, Operand dest, params ReadOnlySpan srcs)
{
Operation result = Make(Instruction.Extended, 0, srcs.Length);
diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs
index cf13cd6cb..3bbec482c 100644
--- a/src/ARMeilleure/Translation/Cache/JitCache.cs
+++ b/src/ARMeilleure/Translation/Cache/JitCache.cs
@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Threading;
namespace ARMeilleure.Translation.Cache
{
@@ -26,7 +27,7 @@ namespace ARMeilleure.Translation.Cache
private static readonly List _cacheEntries = new();
- private static readonly object _lock = new();
+ private static readonly Lock _lock = new();
private static bool _initialized;
[SupportedOSPlatform("windows")]
diff --git a/src/ARMeilleure/Translation/EmitterContext.cs b/src/ARMeilleure/Translation/EmitterContext.cs
index e2d860f82..22b6b9842 100644
--- a/src/ARMeilleure/Translation/EmitterContext.cs
+++ b/src/ARMeilleure/Translation/EmitterContext.cs
@@ -559,27 +559,27 @@ namespace ARMeilleure.Translation
return dest;
}
- public Operand AddIntrinsic(Intrinsic intrin, params Operand[] args)
+ public Operand AddIntrinsic(Intrinsic intrin, params ReadOnlySpan args)
{
return Add(intrin, Local(OperandType.V128), args);
}
- public Operand AddIntrinsicInt(Intrinsic intrin, params Operand[] args)
+ public Operand AddIntrinsicInt(Intrinsic intrin, params ReadOnlySpan args)
{
return Add(intrin, Local(OperandType.I32), args);
}
- public Operand AddIntrinsicLong(Intrinsic intrin, params Operand[] args)
+ public Operand AddIntrinsicLong(Intrinsic intrin, params ReadOnlySpan args)
{
return Add(intrin, Local(OperandType.I64), args);
}
- public void AddIntrinsicNoRet(Intrinsic intrin, params Operand[] args)
+ public void AddIntrinsicNoRet(Intrinsic intrin, params ReadOnlySpan args)
{
Add(intrin, default, args);
}
- private Operand Add(Intrinsic intrin, Operand dest, params Operand[] sources)
+ private Operand Add(Intrinsic intrin, Operand dest, params ReadOnlySpan sources)
{
NewNextBlockIfNeeded();
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 841e5fefa..4675abc49 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -59,7 +59,7 @@ namespace ARMeilleure.Translation.PTC
private readonly ManualResetEvent _waitEvent;
- private readonly object _lock;
+ private readonly Lock _lock = new();
private bool _disposed;
@@ -89,8 +89,6 @@ namespace ARMeilleure.Translation.PTC
_waitEvent = new ManualResetEvent(true);
- _lock = new object();
-
_disposed = false;
TitleIdText = TitleIdTextDefault;
diff --git a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
index 8e95c5e4b..7e630ae10 100644
--- a/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
+++ b/src/ARMeilleure/Translation/PTC/PtcProfiler.cs
@@ -41,7 +41,7 @@ namespace ARMeilleure.Translation.PTC
private readonly ManualResetEvent _waitEvent;
- private readonly object _lock;
+ private readonly Lock _lock = new();
private bool _disposed;
@@ -65,8 +65,6 @@ namespace ARMeilleure.Translation.PTC
_waitEvent = new ManualResetEvent(true);
- _lock = new object();
-
_disposed = false;
ProfiledFuncs = new Dictionary();
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
index 3b9129130..7292450a6 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
@@ -5,6 +5,7 @@ using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading;
namespace Ryujinx.Audio.Backends.OpenAL
{
@@ -18,7 +19,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
private ulong _playedSampleCount;
private float _volume;
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
index bdf46d688..4ef3aebec 100644
--- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
+++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj
@@ -1,7 +1,6 @@
- net8.0$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
index 940e47308..d0d45122e 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
+++ b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
index 671a6ad5e..d06f66181 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
+++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj
@@ -1,7 +1,6 @@
- net8.0truewin-x64;osx-x64;linux-x64$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Audio/AudioManager.cs b/src/Ryujinx.Audio/AudioManager.cs
index 370d3d098..8c2c0ef6b 100644
--- a/src/Ryujinx.Audio/AudioManager.cs
+++ b/src/Ryujinx.Audio/AudioManager.cs
@@ -11,7 +11,7 @@ namespace Ryujinx.Audio
///
/// Lock used to control the waiters registration.
///
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
///
/// Events signaled when the driver played audio buffers.
diff --git a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
index 7aefe8865..6f31755a3 100644
--- a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
+++ b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
@@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Common.Memory;
using System;
using System.Buffers;
+using System.Threading;
namespace Ryujinx.Audio.Backends.Common
{
@@ -12,7 +13,7 @@ namespace Ryujinx.Audio.Backends.Common
{
private const int RingBufferAlignment = 2048;
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
private MemoryOwner _bufferOwner;
private Memory _buffer;
diff --git a/src/Ryujinx.Audio/Input/AudioInputManager.cs b/src/Ryujinx.Audio/Input/AudioInputManager.cs
index d56997e9c..ffc3e6da2 100644
--- a/src/Ryujinx.Audio/Input/AudioInputManager.cs
+++ b/src/Ryujinx.Audio/Input/AudioInputManager.cs
@@ -14,12 +14,12 @@ namespace Ryujinx.Audio.Input
///
public class AudioInputManager : IDisposable
{
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
///
/// Lock used for session allocation.
///
- private readonly object _sessionLock = new();
+ private readonly Lock _sessionLock = new();
///
/// The session ids allocation table.
diff --git a/src/Ryujinx.Audio/Input/AudioInputSystem.cs b/src/Ryujinx.Audio/Input/AudioInputSystem.cs
index 34623b34f..65b99745d 100644
--- a/src/Ryujinx.Audio/Input/AudioInputSystem.cs
+++ b/src/Ryujinx.Audio/Input/AudioInputSystem.cs
@@ -48,7 +48,7 @@ namespace Ryujinx.Audio.Input
///
/// The lock of the parent.
///
- private readonly object _parentLock;
+ private readonly Lock _parentLock;
///
/// The dispose state.
@@ -62,7 +62,7 @@ namespace Ryujinx.Audio.Input
/// The lock of the manager
/// The hardware device session
/// The buffer release event of the audio input
- public AudioInputSystem(AudioInputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
+ public AudioInputSystem(AudioInputManager manager, Lock parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
{
_manager = manager;
_parentLock = parentLock;
diff --git a/src/Ryujinx.Audio/Output/AudioOutputManager.cs b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
index 308cd1564..13e169a24 100644
--- a/src/Ryujinx.Audio/Output/AudioOutputManager.cs
+++ b/src/Ryujinx.Audio/Output/AudioOutputManager.cs
@@ -14,12 +14,12 @@ namespace Ryujinx.Audio.Output
///
public class AudioOutputManager : IDisposable
{
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
///
/// Lock used for session allocation.
///
- private readonly object _sessionLock = new();
+ private readonly Lock _sessionLock = new();
///
/// The session ids allocation table.
diff --git a/src/Ryujinx.Audio/Output/AudioOutputSystem.cs b/src/Ryujinx.Audio/Output/AudioOutputSystem.cs
index f9b9bdcf1..dc7d52ced 100644
--- a/src/Ryujinx.Audio/Output/AudioOutputSystem.cs
+++ b/src/Ryujinx.Audio/Output/AudioOutputSystem.cs
@@ -48,7 +48,7 @@ namespace Ryujinx.Audio.Output
///
/// THe lock of the parent.
///
- private readonly object _parentLock;
+ private readonly Lock _parentLock;
///
/// The dispose state.
@@ -62,7 +62,7 @@ namespace Ryujinx.Audio.Output
/// The lock of the manager
/// The hardware device session
/// The buffer release event of the audio output
- public AudioOutputSystem(AudioOutputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
+ public AudioOutputSystem(AudioOutputManager manager, Lock parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
{
_manager = manager;
_parentLock = parentLock;
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
index 246889c48..65a134af1 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
@@ -26,7 +26,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
public class AudioRenderSystem : IDisposable
{
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
private AudioRendererRenderingDevice _renderingDevice;
private AudioRendererExecutionMode _executionMode;
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
index e334a89f6..6d7db059f 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
@@ -19,12 +19,12 @@ namespace Ryujinx.Audio.Renderer.Server
///
/// Lock used for session allocation.
///
- private readonly object _sessionLock = new();
+ private readonly Lock _sessionLock = new();
///
/// Lock used to control the running state.
///
- private readonly object _audioProcessorLock = new();
+ private readonly Lock _audioProcessorLock = new();
///
/// The session ids allocation table.
diff --git a/src/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs b/src/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs
index dbc2c9b3f..8b3f39439 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Threading;
namespace Ryujinx.Audio.Renderer.Server.Upsampler
{
@@ -16,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Server.Upsampler
///
/// Global lock of the object.
///
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
///
/// The upsamplers instances.
diff --git a/src/Ryujinx.Audio/Ryujinx.Audio.csproj b/src/Ryujinx.Audio/Ryujinx.Audio.csproj
index 8901bbf59..92e0fe93f 100644
--- a/src/Ryujinx.Audio/Ryujinx.Audio.csproj
+++ b/src/Ryujinx.Audio/Ryujinx.Audio.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs
new file mode 100644
index 000000000..6dc3d8aa8
--- /dev/null
+++ b/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs
@@ -0,0 +1,73 @@
+using System;
+using Microsoft.Build.Utilities;
+using System.Collections.Generic;
+using System.Linq;
+using System.IO;
+using Newtonsoft.Json;
+using Microsoft.Build.Framework;
+
+namespace Ryujinx.BuildValidationTasks
+{
+ public class LocaleValidationTask : Task
+ {
+ public override bool Execute()
+ {
+ string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
+
+ if (path.Split(["src"], StringSplitOptions.None).Length == 1)
+ {
+ //i assume that we are in a build directory in the solution dir
+ path = new FileInfo(path).Directory!.Parent!.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName;
+ }
+ else
+ {
+ path = path.Split(["src"], StringSplitOptions.None)[0];
+ path = new FileInfo(path).Directory!.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName;
+ }
+
+ string data;
+
+ using (StreamReader sr = new(path))
+ {
+ data = sr.ReadToEnd();
+ }
+
+ LocalesJson json = JsonConvert.DeserializeObject(data);
+
+ for (int i = 0; i < json.Locales.Count; i++)
+ {
+ LocalesEntry locale = json.Locales[i];
+
+ foreach (string langCode in json.Languages.Where(it => !locale.Translations.ContainsKey(it)))
+ {
+ locale.Translations.Add(langCode, string.Empty);
+ Log.LogMessage(MessageImportance.High, $"Added '{langCode}' to Locale '{locale.ID}'");
+ }
+
+ locale.Translations = locale.Translations.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
+ json.Locales[i] = locale;
+ }
+
+ string jsonString = JsonConvert.SerializeObject(json, Formatting.Indented);
+
+ using (StreamWriter sw = new(path))
+ {
+ sw.Write(jsonString);
+ }
+
+ return true;
+ }
+
+ struct LocalesJson
+ {
+ public List Languages { get; set; }
+ public List Locales { get; set; }
+ }
+
+ struct LocalesEntry
+ {
+ public string ID { get; set; }
+ public Dictionary Translations { get; set; }
+ }
+ }
+}
diff --git a/src/Ryujinx.BuildValidationTasks/Ryujinx.BuildValidationTasks.csproj b/src/Ryujinx.BuildValidationTasks/Ryujinx.BuildValidationTasks.csproj
new file mode 100644
index 000000000..dbd9492df
--- /dev/null
+++ b/src/Ryujinx.BuildValidationTasks/Ryujinx.BuildValidationTasks.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.0
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
index e3b4f91b0..9d399d560 100644
--- a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
+++ b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
@@ -6,7 +6,9 @@ namespace Ryujinx.Common.Configuration
[JsonConverter(typeof(TypedStringEnumConverter))]
public enum GraphicsBackend
{
+ Auto,
Vulkan,
OpenGl,
+ Metal
}
}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs
index 608681551..076530744 100644
--- a/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs
@@ -1,6 +1,8 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
- public class JoyconConfigControllerStick where TButton : unmanaged where TStick : unmanaged
+ public class JoyconConfigControllerStick
+ where TButton : unmanaged
+ where TStick : unmanaged
{
public TStick Joystick { get; set; }
public bool InvertStickX { get; set; }
diff --git a/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs b/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
index c0973dcb3..45b8e95fa 100644
--- a/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
+++ b/src/Ryujinx.Common/PreciseSleep/NanosleepPool.cs
@@ -124,7 +124,7 @@ namespace Ryujinx.Common.PreciseSleep
}
}
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
private readonly List _threads = new();
private readonly List _active = new();
private readonly Stack _free = new();
diff --git a/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
index 3bf092704..cef4dc927 100644
--- a/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
+++ b/src/Ryujinx.Common/PreciseSleep/WindowsGranularTimer.cs
@@ -50,7 +50,7 @@ namespace Ryujinx.Common.SystemInterop
private long _lastTicks = PerformanceCounter.ElapsedTicks;
private long _lastId;
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
private readonly List _waitingObjects = new();
private WindowsGranularTimer()
diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs
index 011d9848a..cbf93013f 100644
--- a/src/Ryujinx.Common/ReleaseInformation.cs
+++ b/src/Ryujinx.Common/ReleaseInformation.cs
@@ -6,7 +6,6 @@ namespace Ryujinx.Common
// DO NOT EDIT, filled by CI
public static class ReleaseInformation
{
- private const string FlatHubChannel = "flathub";
private const string CanaryChannel = "canary";
private const string ReleaseChannel = "release";
@@ -29,8 +28,6 @@ namespace Ryujinx.Common
!ReleaseChannelRepo.StartsWith("%%") &&
!ConfigFileName.StartsWith("%%");
- public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannel);
-
public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel);
public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel);
diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj
index 85d4b58bd..de163aae7 100644
--- a/src/Ryujinx.Common/Ryujinx.Common.csproj
+++ b/src/Ryujinx.Common/Ryujinx.Common.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefineConstants);$(ExtraDefineConstants)$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Common/Utilities/JsonHelper.cs b/src/Ryujinx.Common/Utilities/JsonHelper.cs
index 276dd5f8c..82eeaddc1 100644
--- a/src/Ryujinx.Common/Utilities/JsonHelper.cs
+++ b/src/Ryujinx.Common/Utilities/JsonHelper.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Text;
using System.Text.Json;
@@ -27,9 +28,14 @@ namespace Ryujinx.Common.Utilities
ReadCommentHandling = JsonCommentHandling.Skip
};
- public static string Serialize(T value, JsonTypeInfo typeInfo) => JsonSerializer.Serialize(value, typeInfo);
+ public static string Serialize(T value, JsonTypeInfo typeInfo)
+ => JsonSerializer.Serialize(value, typeInfo);
- public static T Deserialize(string value, JsonTypeInfo typeInfo) => JsonSerializer.Deserialize(value, typeInfo);
+ public static T Deserialize(string value, JsonTypeInfo typeInfo)
+ => JsonSerializer.Deserialize(value, typeInfo);
+
+ public static T Deserialize(ReadOnlySpan utf8Value, JsonTypeInfo typeInfo)
+ => JsonSerializer.Deserialize(utf8Value, typeInfo);
public static void SerializeToFile(string filePath, T value, JsonTypeInfo typeInfo)
{
diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
index bb56a4344..74c39d6a8 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs
@@ -230,25 +230,20 @@ namespace Ryujinx.Cpu.AppleHv
{
if (size == 0)
{
- return Enumerable.Empty();
+ yield break;
}
var guestRegions = GetPhysicalRegionsImpl(va, size);
if (guestRegions == null)
{
- return null;
+ yield break;
}
- var regions = new HostMemoryRange[guestRegions.Count];
-
- for (int i = 0; i < regions.Length; i++)
+ foreach (var guestRegion in guestRegions)
{
- var guestRegion = guestRegions[i];
nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size);
- regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size);
+ yield return new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size);
}
-
- return regions;
}
///
@@ -256,23 +251,24 @@ namespace Ryujinx.Cpu.AppleHv
{
if (size == 0)
{
- return Enumerable.Empty();
+ yield break;
}
- return GetPhysicalRegionsImpl(va, size);
+ foreach (var physicalRegion in GetPhysicalRegionsImpl(va, size))
+ {
+ yield return physicalRegion;
+ }
}
- private List GetPhysicalRegionsImpl(ulong va, ulong size)
+ private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{
- return null;
+ yield break;
}
int pages = GetPagesCount(va, (uint)size, out va);
- var regions = new List();
-
ulong regionStart = GetPhysicalAddressInternal(va);
ulong regionSize = PageSize;
@@ -280,14 +276,14 @@ namespace Ryujinx.Cpu.AppleHv
{
if (!ValidateAddress(va + PageSize))
{
- return null;
+ yield break;
}
ulong newPa = GetPhysicalAddressInternal(va + PageSize);
if (GetPhysicalAddressInternal(va) + PageSize != newPa)
{
- regions.Add(new MemoryRange(regionStart, regionSize));
+ yield return new MemoryRange(regionStart, regionSize);
regionStart = newPa;
regionSize = 0;
}
@@ -296,9 +292,7 @@ namespace Ryujinx.Cpu.AppleHv
regionSize += PageSize;
}
- regions.Add(new MemoryRange(regionStart, regionSize));
-
- return regions;
+ yield return new MemoryRange(regionStart, regionSize);
}
///
diff --git a/src/Ryujinx.Cpu/AppleHv/HvVm.cs b/src/Ryujinx.Cpu/AppleHv/HvVm.cs
index a12bbea9b..b4d45f36d 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvVm.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvVm.cs
@@ -1,6 +1,7 @@
using Ryujinx.Memory;
using System;
using System.Runtime.Versioning;
+using System.Threading;
namespace Ryujinx.Cpu.AppleHv
{
@@ -12,7 +13,7 @@ namespace Ryujinx.Cpu.AppleHv
private static int _addressSpaces;
private static HvIpaAllocator _ipaAllocator;
- private static readonly object _lock = new();
+ private static readonly Lock _lock = new();
public static (ulong, HvIpaAllocator) CreateAddressSpace(MemoryBlock block)
{
diff --git a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs
index a49e0179d..f39b295cd 100644
--- a/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs
+++ b/src/Ryujinx.Cpu/Jit/HostTracked/AddressSpacePartitionAllocator.cs
@@ -115,6 +115,9 @@ namespace Ryujinx.Cpu.Jit.HostTracked
}
private readonly AddressIntrusiveRedBlackTree _mappingTree;
+
+ // type is not Lock due to the unique usage of this mechanism,
+ // an arbitrary object is used as the lock passed in by constructor.
private readonly object _lock;
public Block(MemoryTracking tracking, Func readPtCallback, MemoryBlock memory, ulong size, object locker) : base(memory, size)
@@ -174,6 +177,9 @@ namespace Ryujinx.Cpu.Jit.HostTracked
private readonly MemoryTracking _tracking;
private readonly Func _readPtCallback;
+
+ // type is not Lock due to the unique usage of this mechanism,
+ // an arbitrary object is used as the lock passed in by constructor.
private readonly object _lock;
public AddressSpacePartitionAllocator(
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
index 049e508d0..076fb6ad8 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs
@@ -250,25 +250,20 @@ namespace Ryujinx.Cpu.Jit
{
if (size == 0)
{
- return Enumerable.Empty();
+ yield break;
}
var guestRegions = GetPhysicalRegionsImpl(va, size);
if (guestRegions == null)
{
- return null;
+ yield break;
}
- var regions = new HostMemoryRange[guestRegions.Count];
-
- for (int i = 0; i < regions.Length; i++)
+ foreach (var guestRegion in guestRegions)
{
- var guestRegion = guestRegions[i];
nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size);
- regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size);
+ yield return new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size);
}
-
- return regions;
}
///
@@ -276,23 +271,24 @@ namespace Ryujinx.Cpu.Jit
{
if (size == 0)
{
- return Enumerable.Empty();
+ yield break;
}
- return GetPhysicalRegionsImpl(va, size);
+ foreach (var physicalRegion in GetPhysicalRegionsImpl(va, size))
+ {
+ yield return physicalRegion;
+ }
}
- private List GetPhysicalRegionsImpl(ulong va, ulong size)
+ private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{
- return null;
+ yield break;
}
int pages = GetPagesCount(va, (uint)size, out va);
- var regions = new List();
-
ulong regionStart = GetPhysicalAddressInternal(va);
ulong regionSize = PageSize;
@@ -300,14 +296,14 @@ namespace Ryujinx.Cpu.Jit
{
if (!ValidateAddress(va + PageSize))
{
- return null;
+ yield break;
}
ulong newPa = GetPhysicalAddressInternal(va + PageSize);
if (GetPhysicalAddressInternal(va) + PageSize != newPa)
{
- regions.Add(new MemoryRange(regionStart, regionSize));
+ yield return new MemoryRange(regionStart, regionSize);
regionStart = newPa;
regionSize = 0;
}
@@ -316,9 +312,7 @@ namespace Ryujinx.Cpu.Jit
regionSize += PageSize;
}
- regions.Add(new MemoryRange(regionStart, regionSize));
-
- return regions;
+ yield return new MemoryRange(regionStart, regionSize);
}
///
diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
index 4dab212a7..499f991f2 100644
--- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
+++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs
@@ -475,17 +475,15 @@ namespace Ryujinx.Cpu.Jit
return GetPhysicalRegionsImpl(va, size);
}
- private List GetPhysicalRegionsImpl(ulong va, ulong size)
+ private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{
- return null;
+ yield break;
}
int pages = GetPagesCount(va, (uint)size, out va);
- var regions = new List();
-
ulong regionStart = GetPhysicalAddressInternal(va);
ulong regionSize = PageSize;
@@ -493,14 +491,14 @@ namespace Ryujinx.Cpu.Jit
{
if (!ValidateAddress(va + PageSize))
{
- return null;
+ yield break;
}
ulong newPa = GetPhysicalAddressInternal(va + PageSize);
if (GetPhysicalAddressInternal(va) + PageSize != newPa)
{
- regions.Add(new MemoryRange(regionStart, regionSize));
+ yield return new MemoryRange(regionStart, regionSize);
regionStart = newPa;
regionSize = 0;
}
@@ -509,9 +507,7 @@ namespace Ryujinx.Cpu.Jit
regionSize += PageSize;
}
- regions.Add(new MemoryRange(regionStart, regionSize));
-
- return regions;
+ yield return new MemoryRange(regionStart, regionSize);
}
///
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs
index 4d97a2264..21e40b6aa 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitSystem.cs
@@ -478,7 +478,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
bool skipContext,
int spillBaseOffset,
int? resultRegister,
- params ulong[] callArgs)
+ params ReadOnlySpan callArgs)
{
uint resultMask = 0u;
diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
index f534e8b6e..bf9338400 100644
--- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs
@@ -307,7 +307,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
int tempRegister;
int tempGuestAddress = -1;
- bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
+ bool inlineLookup = guestAddress.Kind != OperandKind.Constant &&
funcTable is { Sparse: true };
if (guestAddress.Kind == OperandKind.Constant)
@@ -417,7 +417,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
nint funcPtr,
int spillBaseOffset,
int? resultRegister,
- params ulong[] callArgs)
+ params ReadOnlySpan callArgs)
{
uint resultMask = 0u;
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
index ac1274bf6..10ae050b6 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Threading;
namespace Ryujinx.Cpu.LightningJit.Cache
{
@@ -23,7 +24,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private static readonly List _cacheEntries = new();
- private static readonly object _lock = new();
+ private static readonly Lock _lock = new();
private static bool _initialized;
[SupportedOSPlatform("windows")]
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
index 3cf279ae3..e9a342aba 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
@@ -4,6 +4,7 @@ using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading;
namespace Ryujinx.Cpu.LightningJit.Cache
{
@@ -104,7 +105,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private readonly MemoryCache _sharedCache;
private readonly MemoryCache _localCache;
private readonly PageAlignedRangeList _pendingMap;
- private readonly object _lock;
+ private readonly Lock _lock = new();
class ThreadLocalCacheEntry
{
@@ -137,7 +138,6 @@ namespace Ryujinx.Cpu.LightningJit.Cache
_sharedCache = new(allocator, SharedCacheSize);
_localCache = new(allocator, LocalCacheSize);
_pendingMap = new(_sharedCache.ReprotectAsRx, RegisterFunction);
- _lock = new();
}
public unsafe nint Map(nint framePointer, ReadOnlySpan code, ulong guestAddress, ulong guestSize)
diff --git a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs
index 3ce7c4f9c..1432c4598 100644
--- a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs
+++ b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs
@@ -8,8 +8,6 @@ namespace Ryujinx.Cpu.LightningJit.CodeGen.Arm64
{
public IEnumerable GetCallStack(nint framePointer, nint codeRegionStart, int codeRegionSize, nint codeRegion2Start, int codeRegion2Size)
{
- List functionPointers = new();
-
while (true)
{
nint functionPointer = Marshal.ReadIntPtr(framePointer, nint.Size);
@@ -20,11 +18,9 @@ namespace Ryujinx.Cpu.LightningJit.CodeGen.Arm64
break;
}
- functionPointers.Add((ulong)functionPointer - 4);
+ yield return (ulong)functionPointer - 4;
framePointer = Marshal.ReadIntPtr(framePointer);
}
-
- return functionPointers;
}
}
}
diff --git a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
index 0a55a7dea..e58a2ce97 100644
--- a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
+++ b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
index 2985f1c21..75a6d3bf8 100644
--- a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
+++ b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Threading;
namespace Ryujinx.Cpu.Signal
{
@@ -59,7 +60,7 @@ namespace Ryujinx.Cpu.Signal
private static MemoryBlock _codeBlock;
- private static readonly object _lock = new();
+ private static readonly Lock _lock = new();
private static bool _initialized;
static NativeSignalHandler()
diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
index 58f54de7d..cbe276355 100644
--- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
+++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj
@@ -1,7 +1,6 @@
- net8.0$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Graphics.GAL/ComputeSize.cs b/src/Ryujinx.Graphics.GAL/ComputeSize.cs
new file mode 100644
index 000000000..ce9c2531c
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/ComputeSize.cs
@@ -0,0 +1,18 @@
+namespace Ryujinx.Graphics.GAL
+{
+ public readonly struct ComputeSize
+ {
+ public readonly static ComputeSize VtgAsCompute = new ComputeSize(32, 32, 1);
+
+ public readonly int X;
+ public readonly int Y;
+ public readonly int Z;
+
+ public ComputeSize(int x, int y, int z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs
index 17c42d2d4..b1eb68f72 100644
--- a/src/Ryujinx.Graphics.GAL/Format.cs
+++ b/src/Ryujinx.Graphics.GAL/Format.cs
@@ -339,6 +339,84 @@ namespace Ryujinx.Graphics.GAL
return 1;
}
+ ///
+ /// Get bytes per element for this format.
+ ///
+ /// Texture format
+ /// Byte size for an element of this format (pixel, vertex attribute, etc)
+ public static int GetBytesPerElement(this Format format)
+ {
+ int scalarSize = format.GetScalarSize();
+
+ switch (format)
+ {
+ case Format.R8G8Unorm:
+ case Format.R8G8Snorm:
+ case Format.R8G8Uint:
+ case Format.R8G8Sint:
+ case Format.R8G8Uscaled:
+ case Format.R8G8Sscaled:
+ case Format.R16G16Float:
+ case Format.R16G16Unorm:
+ case Format.R16G16Snorm:
+ case Format.R16G16Uint:
+ case Format.R16G16Sint:
+ case Format.R16G16Uscaled:
+ case Format.R16G16Sscaled:
+ case Format.R32G32Float:
+ case Format.R32G32Uint:
+ case Format.R32G32Sint:
+ case Format.R32G32Uscaled:
+ case Format.R32G32Sscaled:
+ return 2 * scalarSize;
+
+ case Format.R8G8B8Unorm:
+ case Format.R8G8B8Snorm:
+ case Format.R8G8B8Uint:
+ case Format.R8G8B8Sint:
+ case Format.R8G8B8Uscaled:
+ case Format.R8G8B8Sscaled:
+ case Format.R16G16B16Float:
+ case Format.R16G16B16Unorm:
+ case Format.R16G16B16Snorm:
+ case Format.R16G16B16Uint:
+ case Format.R16G16B16Sint:
+ case Format.R16G16B16Uscaled:
+ case Format.R16G16B16Sscaled:
+ case Format.R32G32B32Float:
+ case Format.R32G32B32Uint:
+ case Format.R32G32B32Sint:
+ case Format.R32G32B32Uscaled:
+ case Format.R32G32B32Sscaled:
+ return 3 * scalarSize;
+
+ case Format.R8G8B8A8Unorm:
+ case Format.R8G8B8A8Snorm:
+ case Format.R8G8B8A8Uint:
+ case Format.R8G8B8A8Sint:
+ case Format.R8G8B8A8Srgb:
+ case Format.R8G8B8A8Uscaled:
+ case Format.R8G8B8A8Sscaled:
+ case Format.B8G8R8A8Unorm:
+ case Format.B8G8R8A8Srgb:
+ case Format.R16G16B16A16Float:
+ case Format.R16G16B16A16Unorm:
+ case Format.R16G16B16A16Snorm:
+ case Format.R16G16B16A16Uint:
+ case Format.R16G16B16A16Sint:
+ case Format.R16G16B16A16Uscaled:
+ case Format.R16G16B16A16Sscaled:
+ case Format.R32G32B32A32Float:
+ case Format.R32G32B32A32Uint:
+ case Format.R32G32B32A32Sint:
+ case Format.R32G32B32A32Uscaled:
+ case Format.R32G32B32A32Sscaled:
+ return 4 * scalarSize;
+ }
+
+ return scalarSize;
+ }
+
///
/// Checks if the texture format is a depth or depth-stencil format.
///
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
index 0bd3dc592..6375d290c 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
@@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public uint ProgramCount { get; set; } = 0;
private Action _interruptAction;
- private readonly object _interruptLock = new();
+ private readonly Lock _interruptLock = new();
public event EventHandler ScreenCaptured;
diff --git a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
index a230296c1..b94a4ec90 100644
--- a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
+++ b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj
@@ -1,7 +1,6 @@
- net8.0$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
index 2fd3227dc..c7965a03d 100644
--- a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
+++ b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
@@ -4,23 +4,22 @@ namespace Ryujinx.Graphics.GAL
{
public int FragmentOutputMap { get; }
public ResourceLayout ResourceLayout { get; }
+ public ComputeSize ComputeLocalSize { get; }
public ProgramPipelineState? State { get; }
public bool FromCache { get; set; }
- public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, ProgramPipelineState state, bool fromCache = false)
+ public ShaderInfo(
+ int fragmentOutputMap,
+ ResourceLayout resourceLayout,
+ ComputeSize computeLocalSize,
+ ProgramPipelineState? state,
+ bool fromCache = false)
{
FragmentOutputMap = fragmentOutputMap;
ResourceLayout = resourceLayout;
+ ComputeLocalSize = computeLocalSize;
State = state;
FromCache = fromCache;
}
-
- public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, bool fromCache = false)
- {
- FragmentOutputMap = fragmentOutputMap;
- ResourceLayout = resourceLayout;
- State = null;
- FromCache = fromCache;
- }
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
index 6de50fb2e..34f2cfcad 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
@@ -11,8 +11,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
///
class VtgAsComputeContext : IDisposable
{
- private const int DummyBufferSize = 16;
-
private readonly GpuContext _context;
///
@@ -48,7 +46,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
1,
1,
1,
- 1,
+ format.GetBytesPerElement(),
format,
DepthStencilMode.Depth,
Target.TextureBuffer,
@@ -521,21 +519,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
return new BufferRange(_geometryIndexDataBuffer.Handle, offset, size, write);
}
- ///
- /// Gets the range for a dummy 16 bytes buffer, filled with zeros.
- ///
- /// Dummy buffer range
- public BufferRange GetDummyBufferRange()
- {
- if (_dummyBuffer == BufferHandle.Null)
- {
- _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory);
- _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0);
- }
-
- return new BufferRange(_dummyBuffer, 0, DummyBufferSize);
- }
-
///
/// Gets the range for a sequential index buffer, with ever incrementing index values.
///
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
index 73682866b..2de324392 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
@@ -147,7 +147,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
_vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount);
_vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0);
- SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format);
continue;
}
@@ -163,15 +162,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
_vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount);
_vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0);
- SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format);
continue;
}
int vbStride = vertexBuffer.UnpackStride();
ulong vbSize = GetVertexBufferSize(address, endAddress.Pack(), vbStride, _indexed, instanced, _firstVertex, _count);
- ulong oldVbSize = vbSize;
-
ulong attributeOffset = (ulong)vertexAttrib.UnpackOffset();
int componentSize = format.GetScalarSize();
@@ -345,20 +341,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
return maxOutputVertices / verticesPerPrimitive;
}
- ///
- /// Binds a dummy buffer as vertex buffer into a buffer texture.
- ///
- /// Shader resource binding reservations
- /// Buffer texture index
- /// Buffer texture format
- private readonly void SetDummyBufferTexture(ResourceReservations reservations, int index, Format format)
- {
- ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format);
- bufferTexture.SetStorage(_vacContext.GetDummyBufferRange());
-
- _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.GetVertexBufferTextureBinding(index), bufferTexture, null);
- }
-
///
/// Binds a vertex buffer into a buffer texture.
///
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 2cfd9af5b..ff7f11142 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -8,6 +8,7 @@ using Ryujinx.Graphics.Texture;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Image
@@ -998,7 +999,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
bool dataOverlaps = texture.DataOverlaps(overlap, compatibility);
- if (!overlap.IsView && dataOverlaps && !incompatibleOverlaps.Exists(incompatible => incompatible.Group == overlap.Group))
+ if (!overlap.IsView && dataOverlaps && !incompatibleOverlaps.Any(incompatible => incompatible.Group == overlap.Group))
{
incompatibleOverlaps.Add(new TextureIncompatibleOverlap(overlap.Group, compatibility));
}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index 526fc0c24..2db5c6290 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -7,6 +7,7 @@ using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Gpu.Image
@@ -1555,7 +1556,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// True if the overlap should register copy dependencies
public void RegisterIncompatibleOverlap(TextureIncompatibleOverlap other, bool copy)
{
- if (!_incompatibleOverlaps.Exists(overlap => overlap.Group == other.Group))
+ if (!_incompatibleOverlaps.Any(overlap => overlap.Group == other.Group))
{
if (copy && other.Compatibility == TextureViewCompatibility.LayoutIncompatible)
{
@@ -1701,3 +1702,4 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
}
+
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
index be7cb0b89..3bf122412 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
@@ -721,7 +721,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// The format of the texture
/// The texture swizzle components
/// The depth-stencil mode
- private static DepthStencilMode GetDepthStencilMode(Format format, params SwizzleComponent[] components)
+ private static DepthStencilMode GetDepthStencilMode(Format format, params ReadOnlySpan components)
{
// R = Depth, G = Stencil.
// On 24-bits depth formats, this is inverted (Stencil is R etc).
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
index d330de638..c5a12c1fc 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
@@ -2,6 +2,7 @@ using Ryujinx.Common.Pools;
using Ryujinx.Memory.Range;
using System;
using System.Linq;
+using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
@@ -76,7 +77,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
private BufferMigration _source;
private BufferModifiedRangeList _migrationTarget;
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
///
/// Whether the modified range list has any entries or not.
@@ -435,7 +436,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (_source == null)
{
- // Create a new migration.
+ // Create a new migration.
_source = new BufferMigration(new BufferMigrationSpan[] { span }, this, _context.SyncNumber);
_context.RegisterBufferMigration(_source);
diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
index 8c740fadc..4cd9c1d5c 100644
--- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
+++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj
@@ -1,7 +1,6 @@
- net8.0true$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index c36fc0ada..b6b811389 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -324,6 +324,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
bool loadHostCache = header.CodeGenVersion == CodeGenVersion;
+ if (context.Capabilities.Api == TargetApi.Metal)
+ {
+ loadHostCache = false;
+ }
+
int programIndex = 0;
DataEntry entry = new();
@@ -392,7 +397,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
context,
shaders,
specState.PipelineState,
- specState.TransformFeedbackDescriptors != null);
+ specState.TransformFeedbackDescriptors != null,
+ specState.ComputeState.GetLocalSize());
IProgram hostProgram;
@@ -629,7 +635,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
return;
}
- WriteHostCode(context, hostCode, program.Shaders, streams, timestamp);
+ if (context.Capabilities.Api != TargetApi.Metal)
+ {
+ WriteHostCode(context, hostCode, program.Shaders, streams, timestamp);
+ }
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
index 20f96462e..74922d1e3 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
@@ -490,7 +490,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
ShaderSource[] shaderSources = new ShaderSource[compilation.TranslatedStages.Length];
- ShaderInfoBuilder shaderInfoBuilder = new(_context, compilation.SpecializationState.TransformFeedbackDescriptors != null);
+ ref GpuChannelComputeState computeState = ref compilation.SpecializationState.ComputeState;
+
+ ShaderInfoBuilder shaderInfoBuilder = new(
+ _context,
+ compilation.SpecializationState.TransformFeedbackDescriptors != null,
+ computeLocalSize: computeState.GetLocalSize());
for (int index = 0; index < compilation.TranslatedStages.Length; index++)
{
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
index 1be75f242..4e9134291 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
@@ -16,7 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly GpuAccessorState _state;
private readonly int _stageIndex;
private readonly bool _compute;
- private readonly bool _isVulkan;
+ private readonly bool _isOpenGL;
private readonly bool _hasGeometryShader;
private readonly bool _supportsQuads;
@@ -38,7 +38,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
_channel = channel;
_state = state;
_stageIndex = stageIndex;
- _isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
+ _isOpenGL = context.Capabilities.Api == TargetApi.OpenGL;
_hasGeometryShader = hasGeometryShader;
_supportsQuads = context.Capabilities.SupportsQuads;
@@ -116,10 +116,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
public GpuGraphicsState QueryGraphicsState()
{
return _state.GraphicsState.CreateShaderGraphicsState(
- !_isVulkan,
+ _isOpenGL,
_supportsQuads,
_hasGeometryShader,
- _isVulkan || _state.GraphicsState.YNegateEnabled);
+ !_isOpenGL || _state.GraphicsState.YNegateEnabled);
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index d89eebabf..701ff764a 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -55,7 +55,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
int binding;
- if (_context.Capabilities.Api == TargetApi.Vulkan)
+ if (_context.Capabilities.Api != TargetApi.OpenGL)
{
binding = GetBindingFromIndex(index, _context.Capabilities.MaximumUniformBuffersPerStage, "Uniform buffer");
}
@@ -71,7 +71,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
int binding;
- if (_context.Capabilities.Api == TargetApi.Vulkan)
+ if (_context.Capabilities.Api != TargetApi.OpenGL)
{
if (count == 1)
{
@@ -103,7 +103,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
int binding;
- if (_context.Capabilities.Api == TargetApi.Vulkan)
+ if (_context.Capabilities.Api != TargetApi.OpenGL)
{
binding = GetBindingFromIndex(index, _context.Capabilities.MaximumStorageBuffersPerStage, "Storage buffer");
}
@@ -119,7 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
int binding;
- if (_context.Capabilities.Api == TargetApi.Vulkan)
+ if (_context.Capabilities.Api != TargetApi.OpenGL)
{
if (count == 1)
{
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs
index d8cdbc348..720f7e796 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs
@@ -1,3 +1,5 @@
+using Ryujinx.Graphics.GAL;
+
namespace Ryujinx.Graphics.Gpu.Shader
{
///
@@ -61,5 +63,14 @@ namespace Ryujinx.Graphics.Gpu.Shader
SharedMemorySize = sharedMemorySize;
HasUnalignedStorageBuffer = hasUnalignedStorageBuffer;
}
+
+ ///
+ /// Gets the local group size of the shader in a GAL compatible struct.
+ ///
+ /// Local group size
+ public ComputeSize GetLocalSize()
+ {
+ return new ComputeSize(LocalSizeX, LocalSizeY, LocalSizeZ);
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
index 4fc66c4c0..0924c60f8 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
@@ -224,7 +224,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
TranslatedShader translatedShader = TranslateShader(_dumper, channel, translatorContext, cachedGuestCode, asCompute: false);
ShaderSource[] shaderSourcesArray = new ShaderSource[] { CreateShaderSource(translatedShader.Program) };
- ShaderInfo info = ShaderInfoBuilder.BuildForCompute(_context, translatedShader.Program.Info);
+ ShaderInfo info = ShaderInfoBuilder.BuildForCompute(
+ _context,
+ translatedShader.Program.Info,
+ computeState.GetLocalSize());
IProgram hostProgram = _context.Renderer.CreateProgram(shaderSourcesArray, info);
cpShader = new CachedShaderProgram(hostProgram, specState, translatedShader.Shader);
@@ -425,7 +428,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
TranslatorContext lastInVertexPipeline = geometryToCompute ? translatorContexts[4] ?? currentStage : currentStage;
- program = lastInVertexPipeline.GenerateVertexPassthroughForCompute();
+ (program, ShaderProgramInfo vacInfo) = lastInVertexPipeline.GenerateVertexPassthroughForCompute();
+ infoBuilder.AddStageInfoVac(vacInfo);
}
else
{
@@ -530,7 +534,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
private ShaderAsCompute CreateHostVertexAsComputeProgram(ShaderProgram program, TranslatorContext context, bool tfEnabled)
{
ShaderSource source = new(program.Code, program.BinaryCode, ShaderStage.Compute, program.Language);
- ShaderInfo info = ShaderInfoBuilder.BuildForVertexAsCompute(_context, program.Info, tfEnabled);
+ ShaderInfo info = ShaderInfoBuilder.BuildForVertexAsCompute(_context, program.Info, context.GetVertexAsComputeInfo(), tfEnabled);
return new(_context.Renderer.CreateProgram(new[] { source }, info), program.Info, context.GetResourceReservations());
}
@@ -822,16 +826,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
/// Creates shader translation options with the requested graphics API and flags.
- /// The shader language is choosen based on the current configuration and graphics API.
+ /// The shader language is chosen based on the current configuration and graphics API.
///
/// Target graphics API
/// Translation flags
/// Translation options
private static TranslationOptions CreateTranslationOptions(TargetApi api, TranslationFlags flags)
{
- TargetLanguage lang = GraphicsConfig.EnableSpirvCompilationOnVulkan && api == TargetApi.Vulkan
- ? TargetLanguage.Spirv
- : TargetLanguage.Glsl;
+ TargetLanguage lang = api switch
+ {
+ TargetApi.OpenGL => TargetLanguage.Glsl,
+ TargetApi.Vulkan => GraphicsConfig.EnableSpirvCompilationOnVulkan ? TargetLanguage.Spirv : TargetLanguage.Glsl,
+ TargetApi.Metal => TargetLanguage.Msl,
+ _ => throw new NotImplementedException()
+ };
return new TranslationOptions(lang, api, flags);
}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
index 49823562f..e283d0832 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
@@ -22,6 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
ResourceStages.Geometry;
private readonly GpuContext _context;
+ private readonly ComputeSize _computeLocalSize;
private int _fragmentOutputMap;
@@ -39,9 +40,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// GPU context that owns the shaders that will be added to the builder
/// Indicates if the graphics shader is used with transform feedback enabled
/// Indicates that the vertex shader will be emulated on a compute shader
- public ShaderInfoBuilder(GpuContext context, bool tfEnabled, bool vertexAsCompute = false)
+ /// Indicates the local thread size for a compute shader
+ public ShaderInfoBuilder(GpuContext context, bool tfEnabled, bool vertexAsCompute = false, ComputeSize computeLocalSize = default)
{
_context = context;
+ _computeLocalSize = computeLocalSize;
_fragmentOutputMap = -1;
@@ -95,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count, bool write = false)
{
AddDescriptor(stages, type, setIndex, start, count);
- AddUsage(stages, type, setIndex, start, count, write);
+ // AddUsage(stages, type, setIndex, start, count, write);
}
///
@@ -159,6 +162,25 @@ namespace Ryujinx.Graphics.Gpu.Shader
AddUsage(info.Images, stages, isImage: true);
}
+ public void AddStageInfoVac(ShaderProgramInfo info)
+ {
+ ResourceStages stages = info.Stage switch
+ {
+ ShaderStage.Compute => ResourceStages.Compute,
+ ShaderStage.Vertex => ResourceStages.Vertex,
+ ShaderStage.TessellationControl => ResourceStages.TessellationControl,
+ ShaderStage.TessellationEvaluation => ResourceStages.TessellationEvaluation,
+ ShaderStage.Geometry => ResourceStages.Geometry,
+ ShaderStage.Fragment => ResourceStages.Fragment,
+ _ => ResourceStages.None,
+ };
+
+ AddUsage(info.CBuffers, stages, isStorage: false);
+ AddUsage(info.SBuffers, stages, isStorage: true);
+ AddUsage(info.Textures, stages, isImage: false);
+ AddUsage(info.Images, stages, isImage: true);
+ }
+
///
/// Adds a resource descriptor to the list of descriptors.
///
@@ -361,14 +383,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
ResourceLayout resourceLayout = new(descriptors.AsReadOnly(), usages.AsReadOnly());
- if (pipeline.HasValue)
- {
- return new ShaderInfo(_fragmentOutputMap, resourceLayout, pipeline.Value, fromCache);
- }
- else
- {
- return new ShaderInfo(_fragmentOutputMap, resourceLayout, fromCache);
- }
+ return new ShaderInfo(_fragmentOutputMap, resourceLayout, _computeLocalSize, pipeline, fromCache);
}
///
@@ -378,14 +393,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Shaders from the disk cache
/// Optional pipeline for background compilation
/// Indicates if the graphics shader is used with transform feedback enabled
+ /// Compute local thread size
/// Shader information
public static ShaderInfo BuildForCache(
GpuContext context,
IEnumerable programs,
ProgramPipelineState? pipeline,
- bool tfEnabled)
+ bool tfEnabled,
+ ComputeSize computeLocalSize)
{
- ShaderInfoBuilder builder = new(context, tfEnabled);
+ ShaderInfoBuilder builder = new(context, tfEnabled, computeLocalSize: computeLocalSize);
foreach (CachedShaderStage program in programs)
{
@@ -403,11 +420,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
/// GPU context that owns the shader
/// Compute shader information
+ /// Compute local thread size
/// True if the compute shader comes from a disk cache, false otherwise
/// Shader information
- public static ShaderInfo BuildForCompute(GpuContext context, ShaderProgramInfo info, bool fromCache = false)
+ public static ShaderInfo BuildForCompute(GpuContext context, ShaderProgramInfo info, ComputeSize computeLocalSize, bool fromCache = false)
{
- ShaderInfoBuilder builder = new(context, tfEnabled: false, vertexAsCompute: false);
+ ShaderInfoBuilder builder = new(context, tfEnabled: false, vertexAsCompute: false, computeLocalSize: computeLocalSize);
builder.AddStageInfo(info);
@@ -422,10 +440,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Indicates if the graphics shader is used with transform feedback enabled
/// True if the compute shader comes from a disk cache, false otherwise
/// Shader information
- public static ShaderInfo BuildForVertexAsCompute(GpuContext context, ShaderProgramInfo info, bool tfEnabled, bool fromCache = false)
+ public static ShaderInfo BuildForVertexAsCompute(GpuContext context, ShaderProgramInfo info, ShaderProgramInfo info2, bool tfEnabled, bool fromCache = false)
{
- ShaderInfoBuilder builder = new(context, tfEnabled, vertexAsCompute: true);
+ ShaderInfoBuilder builder = new(context, tfEnabled, vertexAsCompute: true, computeLocalSize: ComputeSize.VtgAsCompute);
+ builder.AddStageInfoVac(info2);
builder.AddStageInfo(info, vertexAsCompute: true);
return builder.Build(null, fromCache);
diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
index 92077e26a..3000d41de 100644
--- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
+++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj
@@ -1,7 +1,6 @@
- net8.0$(DefaultItemExcludes);._*
diff --git a/src/Ryujinx.Graphics.Metal/Auto.cs b/src/Ryujinx.Graphics.Metal/Auto.cs
new file mode 100644
index 000000000..7e79ecbc3
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Auto.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Metal
+{
+ interface IAuto
+ {
+ bool HasCommandBufferDependency(CommandBufferScoped cbs);
+
+ void IncrementReferenceCount();
+ void DecrementReferenceCount(int cbIndex);
+ void DecrementReferenceCount();
+ }
+
+ interface IAutoPrivate : IAuto
+ {
+ void AddCommandBufferDependencies(CommandBufferScoped cbs);
+ }
+
+ [SupportedOSPlatform("macos")]
+ class Auto : IAutoPrivate, IDisposable where T : IDisposable
+ {
+ private int _referenceCount;
+ private T _value;
+
+ private readonly BitMap _cbOwnership;
+ private readonly MultiFenceHolder _waitable;
+
+ private bool _disposed;
+ private bool _destroyed;
+
+ public Auto(T value)
+ {
+ _referenceCount = 1;
+ _value = value;
+ _cbOwnership = new BitMap(CommandBufferPool.MaxCommandBuffers);
+ }
+
+ public Auto(T value, MultiFenceHolder waitable) : this(value)
+ {
+ _waitable = waitable;
+ }
+
+ public T Get(CommandBufferScoped cbs, int offset, int size, bool write = false)
+ {
+ _waitable?.AddBufferUse(cbs.CommandBufferIndex, offset, size, write);
+ return Get(cbs);
+ }
+
+ public T GetUnsafe()
+ {
+ return _value;
+ }
+
+ public T Get(CommandBufferScoped cbs)
+ {
+ if (!_destroyed)
+ {
+ AddCommandBufferDependencies(cbs);
+ }
+
+ return _value;
+ }
+
+ public bool HasCommandBufferDependency(CommandBufferScoped cbs)
+ {
+ return _cbOwnership.IsSet(cbs.CommandBufferIndex);
+ }
+
+ public bool HasRentedCommandBufferDependency(CommandBufferPool cbp)
+ {
+ return _cbOwnership.AnySet();
+ }
+
+ public void AddCommandBufferDependencies(CommandBufferScoped cbs)
+ {
+ // We don't want to add a reference to this object to the command buffer
+ // more than once, so if we detect that the command buffer already has ownership
+ // of this object, then we can just return without doing anything else.
+ if (_cbOwnership.Set(cbs.CommandBufferIndex))
+ {
+ if (_waitable != null)
+ {
+ cbs.AddWaitable(_waitable);
+ }
+
+ cbs.AddDependant(this);
+ }
+ }
+
+ public bool TryIncrementReferenceCount()
+ {
+ int lastValue;
+ do
+ {
+ lastValue = _referenceCount;
+
+ if (lastValue == 0)
+ {
+ return false;
+ }
+ }
+ while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue);
+
+ return true;
+ }
+
+ public void IncrementReferenceCount()
+ {
+ if (Interlocked.Increment(ref _referenceCount) == 1)
+ {
+ Interlocked.Decrement(ref _referenceCount);
+ throw new InvalidOperationException("Attempted to increment the reference count of an object that was already destroyed.");
+ }
+ }
+
+ public void DecrementReferenceCount(int cbIndex)
+ {
+ _cbOwnership.Clear(cbIndex);
+ DecrementReferenceCount();
+ }
+
+ public void DecrementReferenceCount()
+ {
+ if (Interlocked.Decrement(ref _referenceCount) == 0)
+ {
+ _value.Dispose();
+ _value = default;
+ _destroyed = true;
+ }
+
+ Debug.Assert(_referenceCount >= 0);
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ DecrementReferenceCount();
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/BackgroundResources.cs b/src/Ryujinx.Graphics.Metal/BackgroundResources.cs
new file mode 100644
index 000000000..8bf6b92bd
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/BackgroundResources.cs
@@ -0,0 +1,107 @@
+using SharpMetal.Metal;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class BackgroundResource : IDisposable
+ {
+ private readonly MetalRenderer _renderer;
+
+ private CommandBufferPool _pool;
+ private PersistentFlushBuffer _flushBuffer;
+
+ public BackgroundResource(MetalRenderer renderer)
+ {
+ _renderer = renderer;
+ }
+
+ public CommandBufferPool GetPool()
+ {
+ if (_pool == null)
+ {
+ MTLCommandQueue queue = _renderer.BackgroundQueue;
+ _pool = new CommandBufferPool(queue, true);
+ _pool.Initialize(null); // TODO: Proper encoder factory for background render/compute
+ }
+
+ return _pool;
+ }
+
+ public PersistentFlushBuffer GetFlushBuffer()
+ {
+ _flushBuffer ??= new PersistentFlushBuffer(_renderer);
+
+ return _flushBuffer;
+ }
+
+ public void Dispose()
+ {
+ _pool?.Dispose();
+ _flushBuffer?.Dispose();
+ }
+ }
+
+ [SupportedOSPlatform("macos")]
+ class BackgroundResources : IDisposable
+ {
+ private readonly MetalRenderer _renderer;
+
+ private readonly Dictionary _resources;
+
+ public BackgroundResources(MetalRenderer renderer)
+ {
+ _renderer = renderer;
+
+ _resources = new Dictionary();
+ }
+
+ private void Cleanup()
+ {
+ lock (_resources)
+ {
+ foreach (KeyValuePair tuple in _resources)
+ {
+ if (!tuple.Key.IsAlive)
+ {
+ tuple.Value.Dispose();
+ _resources.Remove(tuple.Key);
+ }
+ }
+ }
+ }
+
+ public BackgroundResource Get()
+ {
+ Thread thread = Thread.CurrentThread;
+
+ lock (_resources)
+ {
+ if (!_resources.TryGetValue(thread, out BackgroundResource resource))
+ {
+ Cleanup();
+
+ resource = new BackgroundResource(_renderer);
+
+ _resources[thread] = resource;
+ }
+
+ return resource;
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_resources)
+ {
+ foreach (var resource in _resources.Values)
+ {
+ resource.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/BitMap.cs b/src/Ryujinx.Graphics.Metal/BitMap.cs
new file mode 100644
index 000000000..4ddc438c1
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/BitMap.cs
@@ -0,0 +1,157 @@
+namespace Ryujinx.Graphics.Metal
+{
+ readonly struct BitMap
+ {
+ public const int IntSize = 64;
+
+ private const int IntShift = 6;
+ private const int IntMask = IntSize - 1;
+
+ private readonly long[] _masks;
+
+ public BitMap(int count)
+ {
+ _masks = new long[(count + IntMask) / IntSize];
+ }
+
+ public bool AnySet()
+ {
+ for (int i = 0; i < _masks.Length; i++)
+ {
+ if (_masks[i] != 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool IsSet(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ return (_masks[wordIndex] & wordMask) != 0;
+ }
+
+ public bool IsSet(int start, int end)
+ {
+ if (start == end)
+ {
+ return IsSet(start);
+ }
+
+ int startIndex = start >> IntShift;
+ int startBit = start & IntMask;
+ long startMask = -1L << startBit;
+
+ int endIndex = end >> IntShift;
+ int endBit = end & IntMask;
+ long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
+
+ if (startIndex == endIndex)
+ {
+ return (_masks[startIndex] & startMask & endMask) != 0;
+ }
+
+ if ((_masks[startIndex] & startMask) != 0)
+ {
+ return true;
+ }
+
+ for (int i = startIndex + 1; i < endIndex; i++)
+ {
+ if (_masks[i] != 0)
+ {
+ return true;
+ }
+ }
+
+ if ((_masks[endIndex] & endMask) != 0)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Set(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ if ((_masks[wordIndex] & wordMask) != 0)
+ {
+ return false;
+ }
+
+ _masks[wordIndex] |= wordMask;
+
+ return true;
+ }
+
+ public void SetRange(int start, int end)
+ {
+ if (start == end)
+ {
+ Set(start);
+ return;
+ }
+
+ int startIndex = start >> IntShift;
+ int startBit = start & IntMask;
+ long startMask = -1L << startBit;
+
+ int endIndex = end >> IntShift;
+ int endBit = end & IntMask;
+ long endMask = (long)(ulong.MaxValue >> (IntMask - endBit));
+
+ if (startIndex == endIndex)
+ {
+ _masks[startIndex] |= startMask & endMask;
+ }
+ else
+ {
+ _masks[startIndex] |= startMask;
+
+ for (int i = startIndex + 1; i < endIndex; i++)
+ {
+ _masks[i] |= -1;
+ }
+
+ _masks[endIndex] |= endMask;
+ }
+ }
+
+ public void Clear(int bit)
+ {
+ int wordIndex = bit >> IntShift;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ _masks[wordIndex] &= ~wordMask;
+ }
+
+ public void Clear()
+ {
+ for (int i = 0; i < _masks.Length; i++)
+ {
+ _masks[i] = 0;
+ }
+ }
+
+ public void ClearInt(int start, int end)
+ {
+ for (int i = start; i <= end; i++)
+ {
+ _masks[i] = 0;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/BufferHolder.cs b/src/Ryujinx.Graphics.Metal/BufferHolder.cs
new file mode 100644
index 000000000..cc86a403f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/BufferHolder.cs
@@ -0,0 +1,385 @@
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class BufferHolder : IDisposable
+ {
+ private CacheByRange _cachedConvertedBuffers;
+
+ public int Size { get; }
+
+ private readonly IntPtr _map;
+ private readonly MetalRenderer _renderer;
+ private readonly Pipeline _pipeline;
+
+ private readonly MultiFenceHolder _waitable;
+ private readonly Auto _buffer;
+
+ private readonly ReaderWriterLockSlim _flushLock;
+ private FenceHolder _flushFence;
+ private int _flushWaiting;
+
+ public BufferHolder(MetalRenderer renderer, Pipeline pipeline, MTLBuffer buffer, int size)
+ {
+ _renderer = renderer;
+ _pipeline = pipeline;
+ _map = buffer.Contents;
+ _waitable = new MultiFenceHolder(size);
+ _buffer = new Auto(new(buffer), _waitable);
+
+ _flushLock = new ReaderWriterLockSlim();
+
+ Size = size;
+ }
+
+ public Auto GetBuffer()
+ {
+ return _buffer;
+ }
+
+ public Auto GetBuffer(bool isWrite)
+ {
+ if (isWrite)
+ {
+ SignalWrite(0, Size);
+ }
+
+ return _buffer;
+ }
+
+ public Auto GetBuffer(int offset, int size, bool isWrite)
+ {
+ if (isWrite)
+ {
+ SignalWrite(offset, size);
+ }
+
+ return _buffer;
+ }
+
+ public void SignalWrite(int offset, int size)
+ {
+ if (offset == 0 && size == Size)
+ {
+ _cachedConvertedBuffers.Clear();
+ }
+ else
+ {
+ _cachedConvertedBuffers.ClearRange(offset, size);
+ }
+ }
+
+ private void ClearFlushFence()
+ {
+ // Assumes _flushLock is held as writer.
+
+ if (_flushFence != null)
+ {
+ if (_flushWaiting == 0)
+ {
+ _flushFence.Put();
+ }
+
+ _flushFence = null;
+ }
+ }
+
+ private void WaitForFlushFence()
+ {
+ 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)
+ {
+ var fence = _flushFence;
+ Interlocked.Increment(ref _flushWaiting);
+
+ // Don't wait in the lock.
+
+ _flushLock.ExitWriteLock();
+
+ fence.Wait();
+
+ _flushLock.EnterWriteLock();
+
+ if (Interlocked.Decrement(ref _flushWaiting) == 0)
+ {
+ fence.Put();
+ }
+
+ _flushFence = null;
+ }
+
+ // Assumes the _flushLock is held as reader, returns in same state.
+ _flushLock.ExitWriteLock();
+ _flushLock.EnterReadLock();
+ }
+
+ public PinnedSpan GetData(int offset, int size)
+ {
+ _flushLock.EnterReadLock();
+
+ WaitForFlushFence();
+
+ Span result;
+
+ if (_map != IntPtr.Zero)
+ {
+ result = GetDataStorage(offset, size);
+
+ // Need to be careful here, the buffer can't be unmapped while the data is being used.
+ _buffer.IncrementReferenceCount();
+
+ _flushLock.ExitReadLock();
+
+ return PinnedSpan.UnsafeFromSpan(result, _buffer.DecrementReferenceCount);
+ }
+
+ throw new InvalidOperationException("The buffer is not mapped");
+ }
+
+ public unsafe Span GetDataStorage(int offset, int size)
+ {
+ int mappingSize = Math.Min(size, Size - offset);
+
+ if (_map != IntPtr.Zero)
+ {
+ return new Span((void*)(_map + offset), mappingSize);
+ }
+
+ throw new InvalidOperationException("The buffer is not mapped.");
+ }
+
+ public unsafe void SetData(int offset, ReadOnlySpan data, CommandBufferScoped? cbs = null, bool allowCbsWait = true)
+ {
+ int dataSize = Math.Min(data.Length, Size - offset);
+ if (dataSize == 0)
+ {
+ return;
+ }
+
+ if (_map != IntPtr.Zero)
+ {
+ // If persistently mapped, set the data directly if the buffer is not currently in use.
+ bool isRented = _buffer.HasRentedCommandBufferDependency(_renderer.CommandBufferPool);
+
+ // If the buffer is rented, take a little more time and check if the use overlaps this handle.
+ bool needsFlush = isRented && _waitable.IsBufferRangeInUse(offset, dataSize, false);
+
+ if (!needsFlush)
+ {
+ WaitForFences(offset, dataSize);
+
+ data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize));
+
+ SignalWrite(offset, dataSize);
+
+ return;
+ }
+ }
+
+ if (cbs != null &&
+ cbs.Value.Encoders.CurrentEncoderType == EncoderType.Render &&
+ !(_buffer.HasCommandBufferDependency(cbs.Value) &&
+ _waitable.IsBufferRangeInUse(cbs.Value.CommandBufferIndex, offset, dataSize)))
+ {
+ // If the buffer hasn't been used on the command buffer yet, try to preload the data.
+ // This avoids ending and beginning render passes on each buffer data upload.
+
+ cbs = _pipeline.GetPreloadCommandBuffer();
+ }
+
+ if (allowCbsWait)
+ {
+ _renderer.BufferManager.StagingBuffer.PushData(_renderer.CommandBufferPool, cbs, this, offset, data);
+ }
+ else
+ {
+ bool rentCbs = cbs == null;
+ if (rentCbs)
+ {
+ cbs = _renderer.CommandBufferPool.Rent();
+ }
+
+ if (!_renderer.BufferManager.StagingBuffer.TryPushData(cbs.Value, this, offset, data))
+ {
+ // Need to do a slow upload.
+ BufferHolder srcHolder = _renderer.BufferManager.Create(dataSize);
+ srcHolder.SetDataUnchecked(0, data);
+
+ var srcBuffer = srcHolder.GetBuffer();
+ var dstBuffer = this.GetBuffer(true);
+
+ Copy(cbs.Value, srcBuffer, dstBuffer, 0, offset, dataSize);
+
+ srcHolder.Dispose();
+ }
+
+ if (rentCbs)
+ {
+ cbs.Value.Dispose();
+ }
+ }
+ }
+
+ public unsafe void SetDataUnchecked(int offset, ReadOnlySpan data)
+ {
+ int dataSize = Math.Min(data.Length, Size - offset);
+ if (dataSize == 0)
+ {
+ return;
+ }
+
+ if (_map != IntPtr.Zero)
+ {
+ data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize));
+ }
+ }
+
+ public void SetDataUnchecked(int offset, ReadOnlySpan data) where T : unmanaged
+ {
+ SetDataUnchecked(offset, MemoryMarshal.AsBytes(data));
+ }
+
+ public static void Copy(
+ CommandBufferScoped cbs,
+ Auto src,
+ Auto dst,
+ int srcOffset,
+ int dstOffset,
+ int size,
+ bool registerSrcUsage = true)
+ {
+ var srcBuffer = registerSrcUsage ? src.Get(cbs, srcOffset, size).Value : src.GetUnsafe().Value;
+ var dstbuffer = dst.Get(cbs, dstOffset, size, true).Value;
+
+ cbs.Encoders.EnsureBlitEncoder().CopyFromBuffer(
+ srcBuffer,
+ (ulong)srcOffset,
+ dstbuffer,
+ (ulong)dstOffset,
+ (ulong)size);
+ }
+
+ public void WaitForFences()
+ {
+ _waitable.WaitForFences();
+ }
+
+ public void WaitForFences(int offset, int size)
+ {
+ _waitable.WaitForFences(offset, size);
+ }
+
+ private bool BoundToRange(int offset, ref int size)
+ {
+ if (offset >= Size)
+ {
+ return false;
+ }
+
+ size = Math.Min(Size - offset, size);
+
+ return true;
+ }
+
+ public Auto GetBufferI8ToI16(CommandBufferScoped cbs, int offset, int size)
+ {
+ if (!BoundToRange(offset, ref size))
+ {
+ return null;
+ }
+
+ var key = new I8ToI16CacheKey(_renderer);
+
+ if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder))
+ {
+ holder = _renderer.BufferManager.Create((size * 2 + 3) & ~3);
+
+ _renderer.HelperShader.ConvertI8ToI16(cbs, this, holder, offset, size);
+
+ key.SetBuffer(holder.GetBuffer());
+
+ _cachedConvertedBuffers.Add(offset, size, key, holder);
+ }
+
+ return holder.GetBuffer();
+ }
+
+ public Auto GetBufferTopologyConversion(CommandBufferScoped cbs, int offset, int size, IndexBufferPattern pattern, int indexSize)
+ {
+ if (!BoundToRange(offset, ref size))
+ {
+ return null;
+ }
+
+ var key = new TopologyConversionCacheKey(_renderer, pattern, indexSize);
+
+ if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder))
+ {
+ // The destination index size is always I32.
+
+ int indexCount = size / indexSize;
+
+ int convertedCount = pattern.GetConvertedCount(indexCount);
+
+ holder = _renderer.BufferManager.Create(convertedCount * 4);
+
+ _renderer.HelperShader.ConvertIndexBuffer(cbs, this, holder, pattern, indexSize, offset, indexCount);
+
+ key.SetBuffer(holder.GetBuffer());
+
+ _cachedConvertedBuffers.Add(offset, size, key, holder);
+ }
+
+ return holder.GetBuffer();
+ }
+
+ public bool TryGetCachedConvertedBuffer(int offset, int size, ICacheKey key, out BufferHolder holder)
+ {
+ return _cachedConvertedBuffers.TryGetValue(offset, size, key, out holder);
+ }
+
+ public void AddCachedConvertedBuffer(int offset, int size, ICacheKey key, BufferHolder holder)
+ {
+ _cachedConvertedBuffers.Add(offset, size, key, holder);
+ }
+
+ public void AddCachedConvertedBufferDependency(int offset, int size, ICacheKey key, Dependency dependency)
+ {
+ _cachedConvertedBuffers.AddDependency(offset, size, key, dependency);
+ }
+
+ public void RemoveCachedConvertedBuffer(int offset, int size, ICacheKey key)
+ {
+ _cachedConvertedBuffers.Remove(offset, size, key);
+ }
+
+
+ public void Dispose()
+ {
+ _pipeline.FlushCommandsIfWeightExceeding(_buffer, (ulong)Size);
+
+ _buffer.Dispose();
+ _cachedConvertedBuffers.Dispose();
+
+ _flushLock.EnterWriteLock();
+
+ ClearFlushFence();
+
+ _flushLock.ExitWriteLock();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/BufferManager.cs b/src/Ryujinx.Graphics.Metal/BufferManager.cs
new file mode 100644
index 000000000..07a686223
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/BufferManager.cs
@@ -0,0 +1,237 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ readonly struct ScopedTemporaryBuffer : IDisposable
+ {
+ private readonly BufferManager _bufferManager;
+ private readonly bool _isReserved;
+
+ public readonly BufferRange Range;
+ public readonly BufferHolder Holder;
+
+ public BufferHandle Handle => Range.Handle;
+ public int Offset => Range.Offset;
+
+ public ScopedTemporaryBuffer(BufferManager bufferManager, BufferHolder holder, BufferHandle handle, int offset, int size, bool isReserved)
+ {
+ _bufferManager = bufferManager;
+
+ Range = new BufferRange(handle, offset, size);
+ Holder = holder;
+
+ _isReserved = isReserved;
+ }
+
+ public void Dispose()
+ {
+ if (!_isReserved)
+ {
+ _bufferManager.Delete(Range.Handle);
+ }
+ }
+ }
+
+ [SupportedOSPlatform("macos")]
+ class BufferManager : IDisposable
+ {
+ private readonly IdList _buffers;
+
+ private readonly MTLDevice _device;
+ private readonly MetalRenderer _renderer;
+ private readonly Pipeline _pipeline;
+
+ public int BufferCount { get; private set; }
+
+ public StagingBuffer StagingBuffer { get; }
+
+ public BufferManager(MTLDevice device, MetalRenderer renderer, Pipeline pipeline)
+ {
+ _device = device;
+ _renderer = renderer;
+ _pipeline = pipeline;
+ _buffers = new IdList();
+
+ StagingBuffer = new StagingBuffer(_renderer, this);
+ }
+
+ public BufferHandle Create(nint pointer, int size)
+ {
+ // TODO: This is the wrong Metal method, we need no-copy which SharpMetal isn't giving us.
+ var buffer = _device.NewBuffer(pointer, (ulong)size, MTLResourceOptions.ResourceStorageModeShared);
+
+ if (buffer == IntPtr.Zero)
+ {
+ Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}, and pointer 0x{pointer:X}.");
+
+ return BufferHandle.Null;
+ }
+
+ var holder = new BufferHolder(_renderer, _pipeline, buffer, size);
+
+ BufferCount++;
+
+ ulong handle64 = (uint)_buffers.Add(holder);
+
+ return Unsafe.As(ref handle64);
+ }
+
+ public BufferHandle CreateWithHandle(int size)
+ {
+ return CreateWithHandle(size, out _);
+ }
+
+ public BufferHandle CreateWithHandle(int size, out BufferHolder holder)
+ {
+ holder = Create(size);
+
+ if (holder == null)
+ {
+ return BufferHandle.Null;
+ }
+
+ BufferCount++;
+
+ ulong handle64 = (uint)_buffers.Add(holder);
+
+ return Unsafe.As(ref handle64);
+ }
+
+ public ScopedTemporaryBuffer ReserveOrCreate(CommandBufferScoped cbs, int size)
+ {
+ StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size);
+
+ if (result.HasValue)
+ {
+ return new ScopedTemporaryBuffer(this, result.Value.Buffer, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true);
+ }
+ else
+ {
+ // Create a temporary buffer.
+ BufferHandle handle = CreateWithHandle(size, out BufferHolder holder);
+
+ return new ScopedTemporaryBuffer(this, holder, handle, 0, size, false);
+ }
+ }
+
+ public BufferHolder Create(int size)
+ {
+ var buffer = _device.NewBuffer((ulong)size, MTLResourceOptions.ResourceStorageModeShared);
+
+ if (buffer != IntPtr.Zero)
+ {
+ return new BufferHolder(_renderer, _pipeline, buffer, size);
+ }
+
+ Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}.");
+
+ return null;
+ }
+
+ public Auto GetBuffer(BufferHandle handle, bool isWrite, out int size)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ size = holder.Size;
+ return holder.GetBuffer(isWrite);
+ }
+
+ size = 0;
+ return null;
+ }
+
+ public Auto GetBuffer(BufferHandle handle, int offset, int size, bool isWrite)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ return holder.GetBuffer(offset, size, isWrite);
+ }
+
+ return null;
+ }
+
+ public Auto GetBuffer(BufferHandle handle, bool isWrite)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ return holder.GetBuffer(isWrite);
+ }
+
+ return null;
+ }
+
+ public Auto GetBufferI8ToI16(CommandBufferScoped cbs, BufferHandle handle, int offset, int size)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ return holder.GetBufferI8ToI16(cbs, offset, size);
+ }
+
+ return null;
+ }
+
+ public Auto GetBufferTopologyConversion(CommandBufferScoped cbs, BufferHandle handle, int offset, int size, IndexBufferPattern pattern, int indexSize)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ return holder.GetBufferTopologyConversion(cbs, offset, size, pattern, indexSize);
+ }
+
+ return null;
+ }
+
+ public PinnedSpan GetData(BufferHandle handle, int offset, int size)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ return holder.GetData(offset, size);
+ }
+
+ return new PinnedSpan();
+ }
+
+ public void SetData(BufferHandle handle, int offset, ReadOnlySpan data) where T : unmanaged
+ {
+ SetData(handle, offset, MemoryMarshal.Cast(data), null);
+ }
+
+ public void SetData(BufferHandle handle, int offset, ReadOnlySpan data, CommandBufferScoped? cbs)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ holder.SetData(offset, data, cbs);
+ }
+ }
+
+ public void Delete(BufferHandle handle)
+ {
+ if (TryGetBuffer(handle, out var holder))
+ {
+ holder.Dispose();
+ _buffers.Remove((int)Unsafe.As(ref handle));
+ }
+ }
+
+ private bool TryGetBuffer(BufferHandle handle, out BufferHolder holder)
+ {
+ return _buffers.TryGetValue((int)Unsafe.As(ref handle), out holder);
+ }
+
+ public void Dispose()
+ {
+ StagingBuffer.Dispose();
+
+ foreach (var buffer in _buffers)
+ {
+ buffer.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs
new file mode 100644
index 000000000..379e27407
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs
@@ -0,0 +1,85 @@
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ internal class BufferUsageBitmap
+ {
+ private readonly BitMap _bitmap;
+ private readonly int _size;
+ private readonly int _granularity;
+ private readonly int _bits;
+ private readonly int _writeBitOffset;
+
+ private readonly int _intsPerCb;
+ private readonly int _bitsPerCb;
+
+ public BufferUsageBitmap(int size, int granularity)
+ {
+ _size = size;
+ _granularity = granularity;
+
+ // There are two sets of bits - one for read tracking, and the other for write.
+ int bits = (size + (granularity - 1)) / granularity;
+ _writeBitOffset = bits;
+ _bits = bits << 1;
+
+ _intsPerCb = (_bits + (BitMap.IntSize - 1)) / BitMap.IntSize;
+ _bitsPerCb = _intsPerCb * BitMap.IntSize;
+
+ _bitmap = new BitMap(_bitsPerCb * CommandBufferPool.MaxCommandBuffers);
+ }
+
+ public void Add(int cbIndex, int offset, int size, bool write)
+ {
+ if (size == 0)
+ {
+ return;
+ }
+
+ // Some usages can be out of bounds (vertex buffer on amd), so bound if necessary.
+ if (offset + size > _size)
+ {
+ size = _size - offset;
+ }
+
+ int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0);
+ int start = cbBase + offset / _granularity;
+ int end = cbBase + (offset + size - 1) / _granularity;
+
+ _bitmap.SetRange(start, end);
+ }
+
+ public bool OverlapsWith(int cbIndex, int offset, int size, bool write = false)
+ {
+ if (size == 0)
+ {
+ return false;
+ }
+
+ int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0);
+ int start = cbBase + offset / _granularity;
+ int end = cbBase + (offset + size - 1) / _granularity;
+
+ return _bitmap.IsSet(start, end);
+ }
+
+ public bool OverlapsWith(int offset, int size, bool write)
+ {
+ for (int i = 0; i < CommandBufferPool.MaxCommandBuffers; i++)
+ {
+ if (OverlapsWith(i, offset, size, write))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void Clear(int cbIndex)
+ {
+ _bitmap.ClearInt(cbIndex * _intsPerCb, (cbIndex + 1) * _intsPerCb - 1);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/CacheByRange.cs b/src/Ryujinx.Graphics.Metal/CacheByRange.cs
new file mode 100644
index 000000000..76515808f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/CacheByRange.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ interface ICacheKey : IDisposable
+ {
+ bool KeyEqual(ICacheKey other);
+ }
+
+ [SupportedOSPlatform("macos")]
+ struct I8ToI16CacheKey : ICacheKey
+ {
+ // Used to notify the pipeline that bindings have invalidated on dispose.
+ // private readonly MetalRenderer _renderer;
+ // private Auto _buffer;
+
+ public I8ToI16CacheKey(MetalRenderer renderer)
+ {
+ // _renderer = renderer;
+ // _buffer = null;
+ }
+
+ public readonly bool KeyEqual(ICacheKey other)
+ {
+ return other is I8ToI16CacheKey;
+ }
+
+ public readonly void SetBuffer(Auto buffer)
+ {
+ // _buffer = buffer;
+ }
+
+ public readonly void Dispose()
+ {
+ // TODO: Tell pipeline buffer is dirty!
+ // _renderer.PipelineInternal.DirtyIndexBuffer(_buffer);
+ }
+ }
+
+ [SupportedOSPlatform("macos")]
+ readonly struct TopologyConversionCacheKey : ICacheKey
+ {
+ private readonly IndexBufferPattern _pattern;
+ private readonly int _indexSize;
+
+ // Used to notify the pipeline that bindings have invalidated on dispose.
+ // private readonly MetalRenderer _renderer;
+ // private Auto _buffer;
+
+ public TopologyConversionCacheKey(MetalRenderer renderer, IndexBufferPattern pattern, int indexSize)
+ {
+ // _renderer = renderer;
+ // _buffer = null;
+ _pattern = pattern;
+ _indexSize = indexSize;
+ }
+
+ public readonly bool KeyEqual(ICacheKey other)
+ {
+ return other is TopologyConversionCacheKey entry &&
+ entry._pattern == _pattern &&
+ entry._indexSize == _indexSize;
+ }
+
+ public void SetBuffer(Auto buffer)
+ {
+ // _buffer = buffer;
+ }
+
+ public readonly void Dispose()
+ {
+ // TODO: Tell pipeline buffer is dirty!
+ // _renderer.PipelineInternal.DirtyVertexBuffer(_buffer);
+ }
+ }
+
+ [SupportedOSPlatform("macos")]
+ readonly struct Dependency
+ {
+ private readonly BufferHolder _buffer;
+ private readonly int _offset;
+ private readonly int _size;
+ private readonly ICacheKey _key;
+
+ public Dependency(BufferHolder buffer, int offset, int size, ICacheKey key)
+ {
+ _buffer = buffer;
+ _offset = offset;
+ _size = size;
+ _key = key;
+ }
+
+ public void RemoveFromOwner()
+ {
+ _buffer.RemoveCachedConvertedBuffer(_offset, _size, _key);
+ }
+ }
+
+ [SupportedOSPlatform("macos")]
+ struct CacheByRange where T : IDisposable
+ {
+ private struct Entry
+ {
+ public readonly ICacheKey Key;
+ public readonly T Value;
+ public List DependencyList;
+
+ public Entry(ICacheKey key, T value)
+ {
+ Key = key;
+ Value = value;
+ DependencyList = null;
+ }
+
+ public readonly void InvalidateDependencies()
+ {
+ if (DependencyList != null)
+ {
+ foreach (Dependency dependency in DependencyList)
+ {
+ dependency.RemoveFromOwner();
+ }
+
+ DependencyList.Clear();
+ }
+ }
+ }
+
+ private Dictionary> _ranges;
+
+ public void Add(int offset, int size, ICacheKey key, T value)
+ {
+ List entries = GetEntries(offset, size);
+
+ entries.Add(new Entry(key, value));
+ }
+
+ public void AddDependency(int offset, int size, ICacheKey key, Dependency dependency)
+ {
+ List entries = GetEntries(offset, size);
+
+ for (int i = 0; i < entries.Count; i++)
+ {
+ Entry entry = entries[i];
+
+ if (entry.Key.KeyEqual(key))
+ {
+ if (entry.DependencyList == null)
+ {
+ entry.DependencyList = new List();
+ entries[i] = entry;
+ }
+
+ entry.DependencyList.Add(dependency);
+
+ break;
+ }
+ }
+ }
+
+ public void Remove(int offset, int size, ICacheKey key)
+ {
+ List entries = GetEntries(offset, size);
+
+ for (int i = 0; i < entries.Count; i++)
+ {
+ Entry entry = entries[i];
+
+ if (entry.Key.KeyEqual(key))
+ {
+ entries.RemoveAt(i--);
+
+ DestroyEntry(entry);
+ }
+ }
+
+ if (entries.Count == 0)
+ {
+ _ranges.Remove(PackRange(offset, size));
+ }
+ }
+
+ public bool TryGetValue(int offset, int size, ICacheKey key, out T value)
+ {
+ List entries = GetEntries(offset, size);
+
+ foreach (Entry entry in entries)
+ {
+ if (entry.Key.KeyEqual(key))
+ {
+ value = entry.Value;
+
+ return true;
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ public void Clear()
+ {
+ if (_ranges != null)
+ {
+ foreach (List entries in _ranges.Values)
+ {
+ foreach (Entry entry in entries)
+ {
+ DestroyEntry(entry);
+ }
+ }
+
+ _ranges.Clear();
+ _ranges = null;
+ }
+ }
+
+ public readonly void ClearRange(int offset, int size)
+ {
+ if (_ranges != null && _ranges.Count > 0)
+ {
+ int end = offset + size;
+
+ List toRemove = null;
+
+ foreach (KeyValuePair> range in _ranges)
+ {
+ (int rOffset, int rSize) = UnpackRange(range.Key);
+
+ int rEnd = rOffset + rSize;
+
+ if (rEnd > offset && rOffset < end)
+ {
+ List entries = range.Value;
+
+ foreach (Entry entry in entries)
+ {
+ DestroyEntry(entry);
+ }
+
+ (toRemove ??= new List()).Add(range.Key);
+ }
+ }
+
+ if (toRemove != null)
+ {
+ foreach (ulong range in toRemove)
+ {
+ _ranges.Remove(range);
+ }
+ }
+ }
+ }
+
+ private List GetEntries(int offset, int size)
+ {
+ _ranges ??= new Dictionary>();
+
+ ulong key = PackRange(offset, size);
+
+ if (!_ranges.TryGetValue(key, out List value))
+ {
+ value = new List();
+ _ranges.Add(key, value);
+ }
+
+ return value;
+ }
+
+ private static void DestroyEntry(Entry entry)
+ {
+ entry.Key.Dispose();
+ entry.Value?.Dispose();
+ entry.InvalidateDependencies();
+ }
+
+ private static ulong PackRange(int offset, int size)
+ {
+ return (uint)offset | ((ulong)size << 32);
+ }
+
+ private static (int offset, int size) UnpackRange(ulong range)
+ {
+ return ((int)range, (int)(range >> 32));
+ }
+
+ public void Dispose()
+ {
+ Clear();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs b/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs
new file mode 100644
index 000000000..ec4150030
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs
@@ -0,0 +1,170 @@
+using Ryujinx.Graphics.Metal;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.Versioning;
+
+interface IEncoderFactory
+{
+ MTLRenderCommandEncoder CreateRenderCommandEncoder();
+ MTLComputeCommandEncoder CreateComputeCommandEncoder();
+}
+
+///
+/// Tracks active encoder object for a command buffer.
+///
+[SupportedOSPlatform("macos")]
+class CommandBufferEncoder
+{
+ public EncoderType CurrentEncoderType { get; private set; } = EncoderType.None;
+
+ public MTLBlitCommandEncoder BlitEncoder => new(CurrentEncoder.Value);
+
+ public MTLComputeCommandEncoder ComputeEncoder => new(CurrentEncoder.Value);
+
+ public MTLRenderCommandEncoder RenderEncoder => new(CurrentEncoder.Value);
+
+ internal MTLCommandEncoder? CurrentEncoder { get; private set; }
+
+ private MTLCommandBuffer _commandBuffer;
+ private IEncoderFactory _encoderFactory;
+
+ public void Initialize(MTLCommandBuffer commandBuffer, IEncoderFactory encoderFactory)
+ {
+ _commandBuffer = commandBuffer;
+ _encoderFactory = encoderFactory;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public MTLRenderCommandEncoder EnsureRenderEncoder()
+ {
+ if (CurrentEncoderType != EncoderType.Render)
+ {
+ return BeginRenderPass();
+ }
+
+ return RenderEncoder;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public MTLBlitCommandEncoder EnsureBlitEncoder()
+ {
+ if (CurrentEncoderType != EncoderType.Blit)
+ {
+ return BeginBlitPass();
+ }
+
+ return BlitEncoder;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public MTLComputeCommandEncoder EnsureComputeEncoder()
+ {
+ if (CurrentEncoderType != EncoderType.Compute)
+ {
+ return BeginComputePass();
+ }
+
+ return ComputeEncoder;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetRenderEncoder(out MTLRenderCommandEncoder encoder)
+ {
+ if (CurrentEncoderType != EncoderType.Render)
+ {
+ encoder = default;
+ return false;
+ }
+
+ encoder = RenderEncoder;
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetBlitEncoder(out MTLBlitCommandEncoder encoder)
+ {
+ if (CurrentEncoderType != EncoderType.Blit)
+ {
+ encoder = default;
+ return false;
+ }
+
+ encoder = BlitEncoder;
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetComputeEncoder(out MTLComputeCommandEncoder encoder)
+ {
+ if (CurrentEncoderType != EncoderType.Compute)
+ {
+ encoder = default;
+ return false;
+ }
+
+ encoder = ComputeEncoder;
+ return true;
+ }
+
+ public void EndCurrentPass()
+ {
+ if (CurrentEncoder != null)
+ {
+ switch (CurrentEncoderType)
+ {
+ case EncoderType.Blit:
+ BlitEncoder.EndEncoding();
+ CurrentEncoder = null;
+ break;
+ case EncoderType.Compute:
+ ComputeEncoder.EndEncoding();
+ CurrentEncoder = null;
+ break;
+ case EncoderType.Render:
+ RenderEncoder.EndEncoding();
+ CurrentEncoder = null;
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+
+ CurrentEncoderType = EncoderType.None;
+ }
+ }
+
+ private MTLRenderCommandEncoder BeginRenderPass()
+ {
+ EndCurrentPass();
+
+ var renderCommandEncoder = _encoderFactory.CreateRenderCommandEncoder();
+
+ CurrentEncoder = renderCommandEncoder;
+ CurrentEncoderType = EncoderType.Render;
+
+ return renderCommandEncoder;
+ }
+
+ private MTLBlitCommandEncoder BeginBlitPass()
+ {
+ EndCurrentPass();
+
+ using var descriptor = new MTLBlitPassDescriptor();
+ var blitCommandEncoder = _commandBuffer.BlitCommandEncoder(descriptor);
+
+ CurrentEncoder = blitCommandEncoder;
+ CurrentEncoderType = EncoderType.Blit;
+ return blitCommandEncoder;
+ }
+
+ private MTLComputeCommandEncoder BeginComputePass()
+ {
+ EndCurrentPass();
+
+ var computeCommandEncoder = _encoderFactory.CreateComputeCommandEncoder();
+
+ CurrentEncoder = computeCommandEncoder;
+ CurrentEncoderType = EncoderType.Compute;
+ return computeCommandEncoder;
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs
new file mode 100644
index 000000000..5e7576b37
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs
@@ -0,0 +1,289 @@
+using SharpMetal.Metal;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class CommandBufferPool : IDisposable
+ {
+ public const int MaxCommandBuffers = 16;
+
+ private readonly int _totalCommandBuffers;
+ private readonly int _totalCommandBuffersMask;
+ private readonly MTLCommandQueue _queue;
+ private readonly Thread _owner;
+ private IEncoderFactory _defaultEncoderFactory;
+
+ public bool OwnedByCurrentThread => _owner == Thread.CurrentThread;
+
+ [SupportedOSPlatform("macos")]
+ private struct ReservedCommandBuffer
+ {
+ public bool InUse;
+ public bool InConsumption;
+ public int SubmissionCount;
+ public MTLCommandBuffer CommandBuffer;
+ public CommandBufferEncoder Encoders;
+ public FenceHolder Fence;
+
+ public List Dependants;
+ public List Waitables;
+
+ public void Use(MTLCommandQueue queue, IEncoderFactory stateManager)
+ {
+ MTLCommandBufferDescriptor descriptor = new();
+#if DEBUG
+ descriptor.ErrorOptions = MTLCommandBufferErrorOption.EncoderExecutionStatus;
+#endif
+
+ CommandBuffer = queue.CommandBuffer(descriptor);
+ Fence = new FenceHolder(CommandBuffer);
+
+ Encoders.Initialize(CommandBuffer, stateManager);
+
+ InUse = true;
+ }
+
+ public void Initialize()
+ {
+ Dependants = new List();
+ Waitables = new List();
+ Encoders = new CommandBufferEncoder();
+ }
+ }
+
+ private readonly ReservedCommandBuffer[] _commandBuffers;
+
+ private readonly int[] _queuedIndexes;
+ private int _queuedIndexesPtr;
+ private int _queuedCount;
+ private int _inUseCount;
+
+ public CommandBufferPool(MTLCommandQueue queue, bool isLight = false)
+ {
+ _queue = queue;
+ _owner = Thread.CurrentThread;
+
+ _totalCommandBuffers = isLight ? 2 : MaxCommandBuffers;
+ _totalCommandBuffersMask = _totalCommandBuffers - 1;
+
+ _commandBuffers = new ReservedCommandBuffer[_totalCommandBuffers];
+
+ _queuedIndexes = new int[_totalCommandBuffers];
+ _queuedIndexesPtr = 0;
+ _queuedCount = 0;
+ }
+
+ public void Initialize(IEncoderFactory encoderFactory)
+ {
+ _defaultEncoderFactory = encoderFactory;
+
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ _commandBuffers[i].Initialize();
+ WaitAndDecrementRef(i);
+ }
+ }
+
+ public void AddDependant(int cbIndex, IAuto dependant)
+ {
+ dependant.IncrementReferenceCount();
+ _commandBuffers[cbIndex].Dependants.Add(dependant);
+ }
+
+ public void AddWaitable(MultiFenceHolder waitable)
+ {
+ lock (_commandBuffers)
+ {
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ ref var entry = ref _commandBuffers[i];
+
+ if (entry.InConsumption)
+ {
+ AddWaitable(i, waitable);
+ }
+ }
+ }
+ }
+
+ public void AddInUseWaitable(MultiFenceHolder waitable)
+ {
+ lock (_commandBuffers)
+ {
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ ref var entry = ref _commandBuffers[i];
+
+ if (entry.InUse)
+ {
+ AddWaitable(i, waitable);
+ }
+ }
+ }
+ }
+
+ public void AddWaitable(int cbIndex, MultiFenceHolder waitable)
+ {
+ ref var entry = ref _commandBuffers[cbIndex];
+ if (waitable.AddFence(cbIndex, entry.Fence))
+ {
+ entry.Waitables.Add(waitable);
+ }
+ }
+
+ public bool IsFenceOnRentedCommandBuffer(FenceHolder fence)
+ {
+ lock (_commandBuffers)
+ {
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ ref var entry = ref _commandBuffers[i];
+
+ if (entry.InUse && entry.Fence == fence)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public FenceHolder GetFence(int cbIndex)
+ {
+ return _commandBuffers[cbIndex].Fence;
+ }
+
+ public int GetSubmissionCount(int cbIndex)
+ {
+ return _commandBuffers[cbIndex].SubmissionCount;
+ }
+
+ private int FreeConsumed(bool wait)
+ {
+ int freeEntry = 0;
+
+ while (_queuedCount > 0)
+ {
+ int index = _queuedIndexes[_queuedIndexesPtr];
+
+ ref var entry = ref _commandBuffers[index];
+
+ if (wait || !entry.InConsumption || entry.Fence.IsSignaled())
+ {
+ WaitAndDecrementRef(index);
+
+ wait = false;
+ freeEntry = index;
+
+ _queuedCount--;
+ _queuedIndexesPtr = (_queuedIndexesPtr + 1) % _totalCommandBuffers;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return freeEntry;
+ }
+
+ public CommandBufferScoped ReturnAndRent(CommandBufferScoped cbs)
+ {
+ Return(cbs);
+ return Rent();
+ }
+
+ public CommandBufferScoped Rent()
+ {
+ lock (_commandBuffers)
+ {
+ int cursor = FreeConsumed(_inUseCount + _queuedCount == _totalCommandBuffers);
+
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ ref var entry = ref _commandBuffers[cursor];
+
+ if (!entry.InUse && !entry.InConsumption)
+ {
+ entry.Use(_queue, _defaultEncoderFactory);
+
+ _inUseCount++;
+
+ return new CommandBufferScoped(this, entry.CommandBuffer, entry.Encoders, cursor);
+ }
+
+ cursor = (cursor + 1) & _totalCommandBuffersMask;
+ }
+ }
+
+ throw new InvalidOperationException($"Out of command buffers (In use: {_inUseCount}, queued: {_queuedCount}, total: {_totalCommandBuffers})");
+ }
+
+ public void Return(CommandBufferScoped cbs)
+ {
+ // Ensure the encoder is committed.
+ cbs.Encoders.EndCurrentPass();
+
+ lock (_commandBuffers)
+ {
+ int cbIndex = cbs.CommandBufferIndex;
+
+ ref var entry = ref _commandBuffers[cbIndex];
+
+ Debug.Assert(entry.InUse);
+ Debug.Assert(entry.CommandBuffer.NativePtr == cbs.CommandBuffer.NativePtr);
+ entry.InUse = false;
+ entry.InConsumption = true;
+ entry.SubmissionCount++;
+ _inUseCount--;
+
+ var commandBuffer = entry.CommandBuffer;
+ commandBuffer.Commit();
+
+ int ptr = (_queuedIndexesPtr + _queuedCount) % _totalCommandBuffers;
+ _queuedIndexes[ptr] = cbIndex;
+ _queuedCount++;
+ }
+ }
+
+ private void WaitAndDecrementRef(int cbIndex)
+ {
+ ref var entry = ref _commandBuffers[cbIndex];
+
+ if (entry.InConsumption)
+ {
+ entry.Fence.Wait();
+ entry.InConsumption = false;
+ }
+
+ foreach (var dependant in entry.Dependants)
+ {
+ dependant.DecrementReferenceCount(cbIndex);
+ }
+
+ foreach (var waitable in entry.Waitables)
+ {
+ waitable.RemoveFence(cbIndex);
+ waitable.RemoveBufferUses(cbIndex);
+ }
+
+ entry.Dependants.Clear();
+ entry.Waitables.Clear();
+ entry.Fence?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ for (int i = 0; i < _totalCommandBuffers; i++)
+ {
+ WaitAndDecrementRef(i);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs
new file mode 100644
index 000000000..822f69b46
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs
@@ -0,0 +1,43 @@
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ readonly struct CommandBufferScoped : IDisposable
+ {
+ private readonly CommandBufferPool _pool;
+ public MTLCommandBuffer CommandBuffer { get; }
+ public CommandBufferEncoder Encoders { get; }
+ public int CommandBufferIndex { get; }
+
+ public CommandBufferScoped(CommandBufferPool pool, MTLCommandBuffer commandBuffer, CommandBufferEncoder encoders, int commandBufferIndex)
+ {
+ _pool = pool;
+ CommandBuffer = commandBuffer;
+ Encoders = encoders;
+ CommandBufferIndex = commandBufferIndex;
+ }
+
+ public void AddDependant(IAuto dependant)
+ {
+ _pool.AddDependant(CommandBufferIndex, dependant);
+ }
+
+ public void AddWaitable(MultiFenceHolder waitable)
+ {
+ _pool.AddWaitable(CommandBufferIndex, waitable);
+ }
+
+ public FenceHolder GetFence()
+ {
+ return _pool.GetFence(CommandBufferIndex);
+ }
+
+ public void Dispose()
+ {
+ _pool?.Return(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Constants.cs b/src/Ryujinx.Graphics.Metal/Constants.cs
new file mode 100644
index 000000000..43baf722a
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Constants.cs
@@ -0,0 +1,41 @@
+namespace Ryujinx.Graphics.Metal
+{
+ static class Constants
+ {
+ public const int MaxShaderStages = 5;
+ public const int MaxVertexBuffers = 16;
+ public const int MaxUniformBuffersPerStage = 18;
+ public const int MaxStorageBuffersPerStage = 16;
+ public const int MaxTexturesPerStage = 64;
+ public const int MaxImagesPerStage = 16;
+
+ public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages;
+ public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages;
+ public const int MaxTextureBindings = MaxTexturesPerStage * MaxShaderStages;
+ public const int MaxImageBindings = MaxImagesPerStage * MaxShaderStages;
+ public const int MaxColorAttachments = 8;
+ public const int MaxViewports = 16;
+ // TODO: Check this value
+ public const int MaxVertexAttributes = 31;
+
+ public const int MinResourceAlignment = 16;
+
+ // Must match constants set in shader generation
+ public const uint ZeroBufferIndex = MaxVertexBuffers;
+ public const uint BaseSetIndex = MaxVertexBuffers + 1;
+
+ public const uint ConstantBuffersIndex = BaseSetIndex;
+ public const uint StorageBuffersIndex = BaseSetIndex + 1;
+ public const uint TexturesIndex = BaseSetIndex + 2;
+ public const uint ImagesIndex = BaseSetIndex + 3;
+
+ public const uint ConstantBuffersSetIndex = 0;
+ public const uint StorageBuffersSetIndex = 1;
+ public const uint TexturesSetIndex = 2;
+ public const uint ImagesSetIndex = 3;
+
+ public const uint MaximumBufferArgumentTableEntries = 31;
+
+ public const uint MaximumExtraSets = MaximumBufferArgumentTableEntries - ImagesIndex;
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/CounterEvent.cs b/src/Ryujinx.Graphics.Metal/CounterEvent.cs
new file mode 100644
index 000000000..46b04997e
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/CounterEvent.cs
@@ -0,0 +1,22 @@
+using Ryujinx.Graphics.GAL;
+
+namespace Ryujinx.Graphics.Metal
+{
+ class CounterEvent : ICounterEvent
+ {
+ public CounterEvent()
+ {
+ Invalid = false;
+ }
+
+ public bool Invalid { get; set; }
+ public bool ReserveForHostAccess()
+ {
+ return true;
+ }
+
+ public void Flush() { }
+
+ public void Dispose() { }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs b/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs
new file mode 100644
index 000000000..bb6e4c180
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs
@@ -0,0 +1,68 @@
+using Ryujinx.Graphics.Metal.State;
+using SharpMetal.Metal;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class DepthStencilCache : StateCache
+ {
+ private readonly MTLDevice _device;
+
+ public DepthStencilCache(MTLDevice device)
+ {
+ _device = device;
+ }
+
+ protected override DepthStencilUid GetHash(DepthStencilUid descriptor)
+ {
+ return descriptor;
+ }
+
+ protected override MTLDepthStencilState CreateValue(DepthStencilUid descriptor)
+ {
+ // Create descriptors
+
+ ref StencilUid frontUid = ref descriptor.FrontFace;
+
+ using var frontFaceStencil = new MTLStencilDescriptor
+ {
+ StencilFailureOperation = frontUid.StencilFailureOperation,
+ DepthFailureOperation = frontUid.DepthFailureOperation,
+ DepthStencilPassOperation = frontUid.DepthStencilPassOperation,
+ StencilCompareFunction = frontUid.StencilCompareFunction,
+ ReadMask = frontUid.ReadMask,
+ WriteMask = frontUid.WriteMask
+ };
+
+ ref StencilUid backUid = ref descriptor.BackFace;
+
+ using var backFaceStencil = new MTLStencilDescriptor
+ {
+ StencilFailureOperation = backUid.StencilFailureOperation,
+ DepthFailureOperation = backUid.DepthFailureOperation,
+ DepthStencilPassOperation = backUid.DepthStencilPassOperation,
+ StencilCompareFunction = backUid.StencilCompareFunction,
+ ReadMask = backUid.ReadMask,
+ WriteMask = backUid.WriteMask
+ };
+
+ var mtlDescriptor = new MTLDepthStencilDescriptor
+ {
+ DepthCompareFunction = descriptor.DepthCompareFunction,
+ DepthWriteEnabled = descriptor.DepthWriteEnabled
+ };
+
+ if (descriptor.StencilTestEnabled)
+ {
+ mtlDescriptor.BackFaceStencil = backFaceStencil;
+ mtlDescriptor.FrontFaceStencil = frontFaceStencil;
+ }
+
+ using (mtlDescriptor)
+ {
+ return _device.NewDepthStencilState(mtlDescriptor);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs
new file mode 100644
index 000000000..a2d2247c4
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs
@@ -0,0 +1,26 @@
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ readonly struct DisposableBuffer : IDisposable
+ {
+ public MTLBuffer Value { get; }
+
+ public DisposableBuffer(MTLBuffer buffer)
+ {
+ Value = buffer;
+ }
+
+ public void Dispose()
+ {
+ if (Value != IntPtr.Zero)
+ {
+ Value.SetPurgeableState(MTLPurgeableState.Empty);
+ Value.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/DisposableSampler.cs b/src/Ryujinx.Graphics.Metal/DisposableSampler.cs
new file mode 100644
index 000000000..ba041be89
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/DisposableSampler.cs
@@ -0,0 +1,22 @@
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ readonly struct DisposableSampler : IDisposable
+ {
+ public MTLSamplerState Value { get; }
+
+ public DisposableSampler(MTLSamplerState sampler)
+ {
+ Value = sampler;
+ }
+
+ public void Dispose()
+ {
+ Value.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs b/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs
new file mode 100644
index 000000000..d575d521f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Ryujinx.Graphics.Metal.Effects
+{
+ internal interface IPostProcessingEffect : IDisposable
+ {
+ const int LocalGroupSize = 64;
+ Texture Run(Texture view, int width, int height);
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs b/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs
new file mode 100644
index 000000000..19f1a3c3d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs
@@ -0,0 +1,18 @@
+using Ryujinx.Graphics.GAL;
+using System;
+
+namespace Ryujinx.Graphics.Metal.Effects
+{
+ internal interface IScalingFilter : IDisposable
+ {
+ float Level { get; set; }
+ void Run(
+ Texture view,
+ Texture destinationTexture,
+ Format format,
+ int width,
+ int height,
+ Extents2D source,
+ Extents2D destination);
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/EncoderResources.cs b/src/Ryujinx.Graphics.Metal/EncoderResources.cs
new file mode 100644
index 000000000..562500d76
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/EncoderResources.cs
@@ -0,0 +1,63 @@
+using SharpMetal.Metal;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Metal
+{
+ public struct RenderEncoderBindings
+ {
+ public List Resources = new();
+ public List VertexBuffers = new();
+ public List FragmentBuffers = new();
+
+ public RenderEncoderBindings() { }
+
+ public readonly void Clear()
+ {
+ Resources.Clear();
+ VertexBuffers.Clear();
+ FragmentBuffers.Clear();
+ }
+ }
+
+ public struct ComputeEncoderBindings
+ {
+ public List Resources = new();
+ public List Buffers = new();
+
+ public ComputeEncoderBindings() { }
+
+ public readonly void Clear()
+ {
+ Resources.Clear();
+ Buffers.Clear();
+ }
+ }
+
+ public struct BufferResource
+ {
+ public MTLBuffer Buffer;
+ public ulong Offset;
+ public ulong Binding;
+
+ public BufferResource(MTLBuffer buffer, ulong offset, ulong binding)
+ {
+ Buffer = buffer;
+ Offset = offset;
+ Binding = binding;
+ }
+ }
+
+ public struct Resource
+ {
+ public MTLResource MtlResource;
+ public MTLResourceUsage ResourceUsage;
+ public MTLRenderStages Stages;
+
+ public Resource(MTLResource resource, MTLResourceUsage resourceUsage, MTLRenderStages stages)
+ {
+ MtlResource = resource;
+ ResourceUsage = resourceUsage;
+ Stages = stages;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/EncoderState.cs b/src/Ryujinx.Graphics.Metal/EncoderState.cs
new file mode 100644
index 000000000..34de168a6
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/EncoderState.cs
@@ -0,0 +1,206 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Metal.State;
+using Ryujinx.Graphics.Shader;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [Flags]
+ enum DirtyFlags
+ {
+ None = 0,
+ RenderPipeline = 1 << 0,
+ ComputePipeline = 1 << 1,
+ DepthStencil = 1 << 2,
+ DepthClamp = 1 << 3,
+ DepthBias = 1 << 4,
+ CullMode = 1 << 5,
+ FrontFace = 1 << 6,
+ StencilRef = 1 << 7,
+ Viewports = 1 << 8,
+ Scissors = 1 << 9,
+ Uniforms = 1 << 10,
+ Storages = 1 << 11,
+ Textures = 1 << 12,
+ Images = 1 << 13,
+
+ ArgBuffers = Uniforms | Storages | Textures | Images,
+
+ RenderAll = RenderPipeline | DepthStencil | DepthClamp | DepthBias | CullMode | FrontFace | StencilRef | Viewports | Scissors | ArgBuffers,
+ ComputeAll = ComputePipeline | ArgBuffers,
+ All = RenderAll | ComputeAll,
+ }
+
+ record struct BufferRef
+ {
+ public Auto Buffer;
+ public BufferRange? Range;
+
+ public BufferRef(Auto buffer)
+ {
+ Buffer = buffer;
+ }
+
+ public BufferRef(Auto buffer, ref BufferRange range)
+ {
+ Buffer = buffer;
+ Range = range;
+ }
+ }
+
+ record struct TextureRef
+ {
+ public ShaderStage Stage;
+ public TextureBase Storage;
+ public Auto Sampler;
+ public Format ImageFormat;
+
+ public TextureRef(ShaderStage stage, TextureBase storage, Auto sampler)
+ {
+ Stage = stage;
+ Storage = storage;
+ Sampler = sampler;
+ }
+ }
+
+ record struct ImageRef
+ {
+ public ShaderStage Stage;
+ public Texture Storage;
+
+ public ImageRef(ShaderStage stage, Texture storage)
+ {
+ Stage = stage;
+ Storage = storage;
+ }
+ }
+
+ struct PredrawState
+ {
+ public MTLCullMode CullMode;
+ public DepthStencilUid DepthStencilUid;
+ public PrimitiveTopology Topology;
+ public MTLViewport[] Viewports;
+ }
+
+ struct RenderTargetCopy
+ {
+ public MTLScissorRect[] Scissors;
+ public Texture DepthStencil;
+ public Texture[] RenderTargets;
+ }
+
+ [SupportedOSPlatform("macos")]
+ class EncoderState
+ {
+ public Program RenderProgram = null;
+ public Program ComputeProgram = null;
+
+ public PipelineState Pipeline;
+ public DepthStencilUid DepthStencilUid;
+
+ public readonly record struct ArrayRef(ShaderStage Stage, T Array);
+
+ public readonly BufferRef[] UniformBufferRefs = new BufferRef[Constants.MaxUniformBufferBindings];
+ public readonly BufferRef[] StorageBufferRefs = new BufferRef[Constants.MaxStorageBufferBindings];
+ public readonly TextureRef[] TextureRefs = new TextureRef[Constants.MaxTextureBindings * 2];
+ public readonly ImageRef[] ImageRefs = new ImageRef[Constants.MaxImageBindings * 2];
+
+ public ArrayRef[] TextureArrayRefs = [];
+ public ArrayRef[] ImageArrayRefs = [];
+
+ public ArrayRef[] TextureArrayExtraRefs = [];
+ public ArrayRef[] ImageArrayExtraRefs = [];
+
+ public IndexBufferState IndexBuffer = default;
+
+ public MTLDepthClipMode DepthClipMode = MTLDepthClipMode.Clip;
+
+ public float DepthBias;
+ public float SlopeScale;
+ public float Clamp;
+
+ public int BackRefValue = 0;
+ public int FrontRefValue = 0;
+
+ public PrimitiveTopology Topology = PrimitiveTopology.Triangles;
+ public MTLCullMode CullMode = MTLCullMode.None;
+ public MTLWinding Winding = MTLWinding.CounterClockwise;
+ public bool CullBoth = false;
+
+ public MTLViewport[] Viewports = new MTLViewport[Constants.MaxViewports];
+ public MTLScissorRect[] Scissors = new MTLScissorRect[Constants.MaxViewports];
+
+ // Changes to attachments take recreation!
+ public Texture DepthStencil;
+ public Texture[] RenderTargets = new Texture[Constants.MaxColorAttachments];
+ public ITexture PreMaskDepthStencil = default;
+ public ITexture[] PreMaskRenderTargets;
+ public bool FramebufferUsingColorWriteMask;
+
+ public Array8 StoredBlend;
+ public ColorF BlendColor = new();
+
+ public readonly VertexBufferState[] VertexBuffers = new VertexBufferState[Constants.MaxVertexBuffers];
+ public readonly VertexAttribDescriptor[] VertexAttribs = new VertexAttribDescriptor[Constants.MaxVertexAttributes];
+ // Dirty flags
+ public DirtyFlags Dirty = DirtyFlags.None;
+
+ // Only to be used for present
+ public bool ClearLoadAction = false;
+
+ public RenderEncoderBindings RenderEncoderBindings = new();
+ public ComputeEncoderBindings ComputeEncoderBindings = new();
+
+ public EncoderState()
+ {
+ Pipeline.Initialize();
+ DepthStencilUid.DepthCompareFunction = MTLCompareFunction.Always;
+ }
+
+ public RenderTargetCopy InheritForClear(EncoderState other, bool depth, int singleIndex = -1)
+ {
+ // Inherit render target related information without causing a render encoder split.
+
+ var oldState = new RenderTargetCopy
+ {
+ Scissors = other.Scissors,
+ RenderTargets = other.RenderTargets,
+ DepthStencil = other.DepthStencil
+ };
+
+ Scissors = other.Scissors;
+ RenderTargets = other.RenderTargets;
+ DepthStencil = other.DepthStencil;
+
+ Pipeline.ColorBlendAttachmentStateCount = other.Pipeline.ColorBlendAttachmentStateCount;
+ Pipeline.Internal.ColorBlendState = other.Pipeline.Internal.ColorBlendState;
+ Pipeline.DepthStencilFormat = other.Pipeline.DepthStencilFormat;
+
+ ref var blendStates = ref Pipeline.Internal.ColorBlendState;
+
+ // Mask out irrelevant attachments.
+ for (int i = 0; i < blendStates.Length; i++)
+ {
+ if (depth || (singleIndex != -1 && singleIndex != i))
+ {
+ blendStates[i].WriteMask = MTLColorWriteMask.None;
+ }
+ }
+
+ return oldState;
+ }
+
+ public void Restore(RenderTargetCopy copy)
+ {
+ Scissors = copy.Scissors;
+ RenderTargets = copy.RenderTargets;
+ DepthStencil = copy.DepthStencil;
+
+ Pipeline.Internal.ResetColorState();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs
new file mode 100644
index 000000000..169e0142d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs
@@ -0,0 +1,1789 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Metal.State;
+using Ryujinx.Graphics.Shader;
+using SharpMetal.Metal;
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using BufferAssignment = Ryujinx.Graphics.GAL.BufferAssignment;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ struct EncoderStateManager : IDisposable
+ {
+ private const int ArrayGrowthSize = 16;
+
+ private readonly MTLDevice _device;
+ private readonly Pipeline _pipeline;
+ private readonly BufferManager _bufferManager;
+
+ private readonly DepthStencilCache _depthStencilCache;
+ private readonly MTLDepthStencilState _defaultState;
+
+ private readonly EncoderState _mainState = new();
+ private EncoderState _currentState;
+
+ public readonly IndexBufferState IndexBuffer => _currentState.IndexBuffer;
+ public readonly PrimitiveTopology Topology => _currentState.Topology;
+ public readonly Texture[] RenderTargets => _currentState.RenderTargets;
+ public readonly Texture DepthStencil => _currentState.DepthStencil;
+ public readonly ComputeSize ComputeLocalSize => _currentState.ComputeProgram.ComputeLocalSize;
+
+ // RGBA32F is the biggest format
+ private const int ZeroBufferSize = 4 * 4;
+ private readonly BufferHandle _zeroBuffer;
+
+ public unsafe EncoderStateManager(MTLDevice device, BufferManager bufferManager, Pipeline pipeline)
+ {
+ _device = device;
+ _pipeline = pipeline;
+ _bufferManager = bufferManager;
+
+ _depthStencilCache = new(device);
+ _currentState = _mainState;
+
+ _defaultState = _depthStencilCache.GetOrCreate(_currentState.DepthStencilUid);
+
+ // Zero buffer
+ byte[] zeros = new byte[ZeroBufferSize];
+ fixed (byte* ptr = zeros)
+ {
+ _zeroBuffer = _bufferManager.Create((IntPtr)ptr, ZeroBufferSize);
+ }
+ }
+
+ public readonly void Dispose()
+ {
+ _depthStencilCache.Dispose();
+ }
+
+ private readonly void SignalDirty(DirtyFlags flags)
+ {
+ _currentState.Dirty |= flags;
+ }
+
+ public readonly void SignalRenderDirty()
+ {
+ SignalDirty(DirtyFlags.RenderAll);
+ }
+
+ public readonly void SignalComputeDirty()
+ {
+ SignalDirty(DirtyFlags.ComputeAll);
+ }
+
+ public EncoderState SwapState(EncoderState state, DirtyFlags flags = DirtyFlags.All)
+ {
+ _currentState = state ?? _mainState;
+
+ SignalDirty(flags);
+
+ return _mainState;
+ }
+
+ public PredrawState SavePredrawState()
+ {
+ return new PredrawState
+ {
+ CullMode = _currentState.CullMode,
+ DepthStencilUid = _currentState.DepthStencilUid,
+ Topology = _currentState.Topology,
+ Viewports = _currentState.Viewports.ToArray(),
+ };
+ }
+
+ public readonly void RestorePredrawState(PredrawState state)
+ {
+ _currentState.CullMode = state.CullMode;
+ _currentState.DepthStencilUid = state.DepthStencilUid;
+ _currentState.Topology = state.Topology;
+ _currentState.Viewports = state.Viewports;
+
+ SignalDirty(DirtyFlags.CullMode | DirtyFlags.DepthStencil | DirtyFlags.Viewports);
+ }
+
+ public readonly void SetClearLoadAction(bool clear)
+ {
+ _currentState.ClearLoadAction = clear;
+ }
+
+ public readonly void DirtyTextures()
+ {
+ SignalDirty(DirtyFlags.Textures);
+ }
+
+ public readonly void DirtyImages()
+ {
+ SignalDirty(DirtyFlags.Images);
+ }
+
+ public readonly MTLRenderCommandEncoder CreateRenderCommandEncoder()
+ {
+ // Initialise Pass & State
+ using var renderPassDescriptor = new MTLRenderPassDescriptor();
+
+ for (int i = 0; i < Constants.MaxColorAttachments; i++)
+ {
+ if (_currentState.RenderTargets[i] is Texture tex)
+ {
+ var passAttachment = renderPassDescriptor.ColorAttachments.Object((ulong)i);
+ tex.PopulateRenderPassAttachment(passAttachment);
+ passAttachment.LoadAction = _currentState.ClearLoadAction ? MTLLoadAction.Clear : MTLLoadAction.Load;
+ passAttachment.StoreAction = MTLStoreAction.Store;
+ }
+ }
+
+ var depthAttachment = renderPassDescriptor.DepthAttachment;
+ var stencilAttachment = renderPassDescriptor.StencilAttachment;
+
+ if (_currentState.DepthStencil != null)
+ {
+ switch (_currentState.DepthStencil.GetHandle().PixelFormat)
+ {
+ // Depth Only Attachment
+ case MTLPixelFormat.Depth16Unorm:
+ case MTLPixelFormat.Depth32Float:
+ depthAttachment.Texture = _currentState.DepthStencil.GetHandle();
+ depthAttachment.LoadAction = MTLLoadAction.Load;
+ depthAttachment.StoreAction = MTLStoreAction.Store;
+ break;
+
+ // Stencil Only Attachment
+ case MTLPixelFormat.Stencil8:
+ stencilAttachment.Texture = _currentState.DepthStencil.GetHandle();
+ stencilAttachment.LoadAction = MTLLoadAction.Load;
+ stencilAttachment.StoreAction = MTLStoreAction.Store;
+ break;
+
+ // Combined Attachment
+ case MTLPixelFormat.Depth24UnormStencil8:
+ case MTLPixelFormat.Depth32FloatStencil8:
+ depthAttachment.Texture = _currentState.DepthStencil.GetHandle();
+ depthAttachment.LoadAction = MTLLoadAction.Load;
+ depthAttachment.StoreAction = MTLStoreAction.Store;
+
+ stencilAttachment.Texture = _currentState.DepthStencil.GetHandle();
+ stencilAttachment.LoadAction = MTLLoadAction.Load;
+ stencilAttachment.StoreAction = MTLStoreAction.Store;
+ break;
+ default:
+ Logger.Error?.PrintMsg(LogClass.Gpu, $"Unsupported Depth/Stencil Format: {_currentState.DepthStencil.GetHandle().PixelFormat}!");
+ break;
+ }
+ }
+
+ // Initialise Encoder
+ var renderCommandEncoder = _pipeline.CommandBuffer.RenderCommandEncoder(renderPassDescriptor);
+
+ return renderCommandEncoder;
+ }
+
+ public readonly MTLComputeCommandEncoder CreateComputeCommandEncoder()
+ {
+ using var descriptor = new MTLComputePassDescriptor();
+ var computeCommandEncoder = _pipeline.CommandBuffer.ComputeCommandEncoder(descriptor);
+
+ return computeCommandEncoder;
+ }
+
+ public readonly void RenderResourcesPrepass()
+ {
+ _currentState.RenderEncoderBindings.Clear();
+
+ if ((_currentState.Dirty & DirtyFlags.RenderPipeline) != 0)
+ {
+ SetVertexBuffers(_currentState.VertexBuffers, ref _currentState.RenderEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0)
+ {
+ UpdateAndBind(_currentState.RenderProgram, Constants.ConstantBuffersSetIndex, ref _currentState.RenderEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Storages) != 0)
+ {
+ UpdateAndBind(_currentState.RenderProgram, Constants.StorageBuffersSetIndex, ref _currentState.RenderEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Textures) != 0)
+ {
+ UpdateAndBind(_currentState.RenderProgram, Constants.TexturesSetIndex, ref _currentState.RenderEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Images) != 0)
+ {
+ UpdateAndBind(_currentState.RenderProgram, Constants.ImagesSetIndex, ref _currentState.RenderEncoderBindings);
+ }
+ }
+
+ public readonly void ComputeResourcesPrepass()
+ {
+ _currentState.ComputeEncoderBindings.Clear();
+
+ if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0)
+ {
+ UpdateAndBind(_currentState.ComputeProgram, Constants.ConstantBuffersSetIndex, ref _currentState.ComputeEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Storages) != 0)
+ {
+ UpdateAndBind(_currentState.ComputeProgram, Constants.StorageBuffersSetIndex, ref _currentState.ComputeEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Textures) != 0)
+ {
+ UpdateAndBind(_currentState.ComputeProgram, Constants.TexturesSetIndex, ref _currentState.ComputeEncoderBindings);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Images) != 0)
+ {
+ UpdateAndBind(_currentState.ComputeProgram, Constants.ImagesSetIndex, ref _currentState.ComputeEncoderBindings);
+ }
+ }
+
+ public void RebindRenderState(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ if ((_currentState.Dirty & DirtyFlags.RenderPipeline) != 0)
+ {
+ SetRenderPipelineState(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.DepthStencil) != 0)
+ {
+ SetDepthStencilState(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.DepthClamp) != 0)
+ {
+ SetDepthClamp(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.DepthBias) != 0)
+ {
+ SetDepthBias(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.CullMode) != 0)
+ {
+ SetCullMode(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.FrontFace) != 0)
+ {
+ SetFrontFace(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.StencilRef) != 0)
+ {
+ SetStencilRefValue(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Viewports) != 0)
+ {
+ SetViewports(renderCommandEncoder);
+ }
+
+ if ((_currentState.Dirty & DirtyFlags.Scissors) != 0)
+ {
+ SetScissors(renderCommandEncoder);
+ }
+
+ foreach (var resource in _currentState.RenderEncoderBindings.Resources)
+ {
+ renderCommandEncoder.UseResource(resource.MtlResource, resource.ResourceUsage, resource.Stages);
+ }
+
+ foreach (var buffer in _currentState.RenderEncoderBindings.VertexBuffers)
+ {
+ renderCommandEncoder.SetVertexBuffer(buffer.Buffer, buffer.Offset, buffer.Binding);
+ }
+
+ foreach (var buffer in _currentState.RenderEncoderBindings.FragmentBuffers)
+ {
+ renderCommandEncoder.SetFragmentBuffer(buffer.Buffer, buffer.Offset, buffer.Binding);
+ }
+
+ _currentState.Dirty &= ~DirtyFlags.RenderAll;
+ }
+
+ public readonly void RebindComputeState(MTLComputeCommandEncoder computeCommandEncoder)
+ {
+ if ((_currentState.Dirty & DirtyFlags.ComputePipeline) != 0)
+ {
+ SetComputePipelineState(computeCommandEncoder);
+ }
+
+ foreach (var resource in _currentState.ComputeEncoderBindings.Resources)
+ {
+ computeCommandEncoder.UseResource(resource.MtlResource, resource.ResourceUsage);
+ }
+
+ foreach (var buffer in _currentState.ComputeEncoderBindings.Buffers)
+ {
+ computeCommandEncoder.SetBuffer(buffer.Buffer, buffer.Offset, buffer.Binding);
+ }
+
+ _currentState.Dirty &= ~DirtyFlags.ComputeAll;
+ }
+
+ private readonly void SetRenderPipelineState(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ MTLRenderPipelineState pipelineState = _currentState.Pipeline.CreateRenderPipeline(_device, _currentState.RenderProgram);
+
+ renderCommandEncoder.SetRenderPipelineState(pipelineState);
+
+ renderCommandEncoder.SetBlendColor(
+ _currentState.BlendColor.Red,
+ _currentState.BlendColor.Green,
+ _currentState.BlendColor.Blue,
+ _currentState.BlendColor.Alpha);
+ }
+
+ private readonly void SetComputePipelineState(MTLComputeCommandEncoder computeCommandEncoder)
+ {
+ if (_currentState.ComputeProgram == null)
+ {
+ return;
+ }
+
+ var pipelineState = PipelineState.CreateComputePipeline(_device, _currentState.ComputeProgram);
+
+ computeCommandEncoder.SetComputePipelineState(pipelineState);
+ }
+
+ public readonly void UpdateIndexBuffer(BufferRange buffer, IndexType type)
+ {
+ if (buffer.Handle != BufferHandle.Null)
+ {
+ _currentState.IndexBuffer = new IndexBufferState(buffer.Handle, buffer.Offset, buffer.Size, type);
+ }
+ else
+ {
+ _currentState.IndexBuffer = IndexBufferState.Null;
+ }
+ }
+
+ public readonly void UpdatePrimitiveTopology(PrimitiveTopology topology)
+ {
+ _currentState.Topology = topology;
+ }
+
+ public readonly void UpdateProgram(IProgram program)
+ {
+ Program prg = (Program)program;
+
+ if (prg.VertexFunction == IntPtr.Zero && prg.ComputeFunction == IntPtr.Zero)
+ {
+ if (prg.FragmentFunction == IntPtr.Zero)
+ {
+ Logger.Error?.PrintMsg(LogClass.Gpu, "No compute function");
+ }
+ else
+ {
+ Logger.Error?.PrintMsg(LogClass.Gpu, "No vertex function");
+ }
+ return;
+ }
+
+ if (prg.VertexFunction != IntPtr.Zero)
+ {
+ _currentState.RenderProgram = prg;
+
+ SignalDirty(DirtyFlags.RenderPipeline | DirtyFlags.ArgBuffers);
+ }
+ else if (prg.ComputeFunction != IntPtr.Zero)
+ {
+ _currentState.ComputeProgram = prg;
+
+ SignalDirty(DirtyFlags.ComputePipeline | DirtyFlags.ArgBuffers);
+ }
+ }
+
+ public readonly void UpdateRasterizerDiscard(bool discard)
+ {
+ _currentState.Pipeline.RasterizerDiscardEnable = discard;
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public readonly void UpdateRenderTargets(ITexture[] colors, ITexture depthStencil)
+ {
+ _currentState.FramebufferUsingColorWriteMask = false;
+ UpdateRenderTargetsInternal(colors, depthStencil);
+ }
+
+ public readonly void UpdateRenderTargetColorMasks(ReadOnlySpan componentMask)
+ {
+ ref var blendState = ref _currentState.Pipeline.Internal.ColorBlendState;
+
+ for (int i = 0; i < componentMask.Length; i++)
+ {
+ bool red = (componentMask[i] & (0x1 << 0)) != 0;
+ bool green = (componentMask[i] & (0x1 << 1)) != 0;
+ bool blue = (componentMask[i] & (0x1 << 2)) != 0;
+ bool alpha = (componentMask[i] & (0x1 << 3)) != 0;
+
+ var mask = MTLColorWriteMask.None;
+
+ mask |= red ? MTLColorWriteMask.Red : 0;
+ mask |= green ? MTLColorWriteMask.Green : 0;
+ mask |= blue ? MTLColorWriteMask.Blue : 0;
+ mask |= alpha ? MTLColorWriteMask.Alpha : 0;
+
+ ref ColorBlendStateUid mtlBlend = ref blendState[i];
+
+ // When color write mask is 0, remove all blend state to help the pipeline cache.
+ // Restore it when the mask becomes non-zero.
+ if (mtlBlend.WriteMask != mask)
+ {
+ if (mask == 0)
+ {
+ _currentState.StoredBlend[i] = mtlBlend;
+
+ mtlBlend.Swap(new ColorBlendStateUid());
+ }
+ else if (mtlBlend.WriteMask == 0)
+ {
+ mtlBlend.Swap(_currentState.StoredBlend[i]);
+ }
+ }
+
+ blendState[i].WriteMask = mask;
+ }
+
+ if (_currentState.FramebufferUsingColorWriteMask)
+ {
+ UpdateRenderTargetsInternal(_currentState.PreMaskRenderTargets, _currentState.PreMaskDepthStencil);
+ }
+ else
+ {
+ // Requires recreating pipeline
+ if (_pipeline.CurrentEncoderType == EncoderType.Render)
+ {
+ _pipeline.EndCurrentPass();
+ }
+ }
+ }
+
+ private readonly void UpdateRenderTargetsInternal(ITexture[] colors, ITexture depthStencil)
+ {
+ // TBDR GPUs don't work properly if the same attachment is bound to multiple targets,
+ // due to each attachment being a copy of the real attachment, rather than a direct write.
+ //
+ // Just try to remove duplicate attachments.
+ // Save a copy of the array to rebind when mask changes.
+
+ // Look for textures that are masked out.
+
+ ref PipelineState pipeline = ref _currentState.Pipeline;
+ ref var blendState = ref pipeline.Internal.ColorBlendState;
+
+ pipeline.ColorBlendAttachmentStateCount = (uint)colors.Length;
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ if (colors[i] == null)
+ {
+ continue;
+ }
+
+ var mtlMask = blendState[i].WriteMask;
+
+ for (int j = 0; j < i; j++)
+ {
+ // Check each binding for a duplicate binding before it.
+
+ if (colors[i] == colors[j])
+ {
+ // Prefer the binding with no write mask.
+
+ var mtlMask2 = blendState[j].WriteMask;
+
+ if (mtlMask == 0)
+ {
+ colors[i] = null;
+ MaskOut(colors, depthStencil);
+ }
+ else if (mtlMask2 == 0)
+ {
+ colors[j] = null;
+ MaskOut(colors, depthStencil);
+ }
+ }
+ }
+ }
+
+ _currentState.RenderTargets = new Texture[Constants.MaxColorAttachments];
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ if (colors[i] is not Texture tex)
+ {
+ blendState[i].PixelFormat = MTLPixelFormat.Invalid;
+
+ continue;
+ }
+
+ blendState[i].PixelFormat = tex.GetHandle().PixelFormat; // TODO: cache this
+ _currentState.RenderTargets[i] = tex;
+ }
+
+ if (depthStencil is Texture depthTexture)
+ {
+ pipeline.DepthStencilFormat = depthTexture.GetHandle().PixelFormat; // TODO: cache this
+ _currentState.DepthStencil = depthTexture;
+ }
+ else if (depthStencil == null)
+ {
+ pipeline.DepthStencilFormat = MTLPixelFormat.Invalid;
+ _currentState.DepthStencil = null;
+ }
+
+ // Requires recreating pipeline
+ if (_pipeline.CurrentEncoderType == EncoderType.Render)
+ {
+ _pipeline.EndCurrentPass();
+ }
+ }
+
+ private readonly void MaskOut(ITexture[] colors, ITexture depthStencil)
+ {
+ if (!_currentState.FramebufferUsingColorWriteMask)
+ {
+ _currentState.PreMaskRenderTargets = colors;
+ _currentState.PreMaskDepthStencil = depthStencil;
+ }
+
+ // If true, then the framebuffer must be recreated when the mask changes.
+ _currentState.FramebufferUsingColorWriteMask = true;
+ }
+
+ public readonly void UpdateVertexAttribs(ReadOnlySpan vertexAttribs)
+ {
+ vertexAttribs.CopyTo(_currentState.VertexAttribs);
+
+ // Update the buffers on the pipeline
+ UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs);
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public readonly void UpdateBlendDescriptors(int index, BlendDescriptor blend)
+ {
+ ref var blendState = ref _currentState.Pipeline.Internal.ColorBlendState[index];
+
+ blendState.Enable = blend.Enable;
+ blendState.AlphaBlendOperation = blend.AlphaOp.Convert();
+ blendState.RgbBlendOperation = blend.ColorOp.Convert();
+ blendState.SourceAlphaBlendFactor = blend.AlphaSrcFactor.Convert();
+ blendState.DestinationAlphaBlendFactor = blend.AlphaDstFactor.Convert();
+ blendState.SourceRGBBlendFactor = blend.ColorSrcFactor.Convert();
+ blendState.DestinationRGBBlendFactor = blend.ColorDstFactor.Convert();
+
+ if (blendState.WriteMask == 0)
+ {
+ _currentState.StoredBlend[index] = blendState;
+
+ blendState.Swap(new ColorBlendStateUid());
+ }
+
+ _currentState.BlendColor = blend.BlendConstant;
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public void UpdateStencilState(StencilTestDescriptor stencilTest)
+ {
+ ref DepthStencilUid uid = ref _currentState.DepthStencilUid;
+
+ uid.FrontFace = new StencilUid
+ {
+ StencilFailureOperation = stencilTest.FrontSFail.Convert(),
+ DepthFailureOperation = stencilTest.FrontDpFail.Convert(),
+ DepthStencilPassOperation = stencilTest.FrontDpPass.Convert(),
+ StencilCompareFunction = stencilTest.FrontFunc.Convert(),
+ ReadMask = (uint)stencilTest.FrontFuncMask,
+ WriteMask = (uint)stencilTest.FrontMask
+ };
+
+ uid.BackFace = new StencilUid
+ {
+ StencilFailureOperation = stencilTest.BackSFail.Convert(),
+ DepthFailureOperation = stencilTest.BackDpFail.Convert(),
+ DepthStencilPassOperation = stencilTest.BackDpPass.Convert(),
+ StencilCompareFunction = stencilTest.BackFunc.Convert(),
+ ReadMask = (uint)stencilTest.BackFuncMask,
+ WriteMask = (uint)stencilTest.BackMask
+ };
+
+ uid.StencilTestEnabled = stencilTest.TestEnable;
+
+ UpdateStencilRefValue(stencilTest.FrontFuncRef, stencilTest.BackFuncRef);
+
+ SignalDirty(DirtyFlags.DepthStencil);
+ }
+
+ public readonly void UpdateDepthState(DepthTestDescriptor depthTest)
+ {
+ ref DepthStencilUid uid = ref _currentState.DepthStencilUid;
+
+ uid.DepthCompareFunction = depthTest.TestEnable ? depthTest.Func.Convert() : MTLCompareFunction.Always;
+ uid.DepthWriteEnabled = depthTest.TestEnable && depthTest.WriteEnable;
+
+ SignalDirty(DirtyFlags.DepthStencil);
+ }
+
+ public readonly void UpdateDepthClamp(bool clamp)
+ {
+ _currentState.DepthClipMode = clamp ? MTLDepthClipMode.Clamp : MTLDepthClipMode.Clip;
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetDepthClamp(renderCommandEncoder);
+ return;
+ }
+
+ SignalDirty(DirtyFlags.DepthClamp);
+ }
+
+ public readonly void UpdateDepthBias(float depthBias, float slopeScale, float clamp)
+ {
+ _currentState.DepthBias = depthBias;
+ _currentState.SlopeScale = slopeScale;
+ _currentState.Clamp = clamp;
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetDepthBias(renderCommandEncoder);
+ return;
+ }
+
+ SignalDirty(DirtyFlags.DepthBias);
+ }
+
+ public readonly void UpdateLogicOpState(bool enable, LogicalOp op)
+ {
+ _currentState.Pipeline.LogicOpEnable = enable;
+ _currentState.Pipeline.LogicOp = op.Convert();
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public readonly void UpdateMultisampleState(MultisampleDescriptor multisample)
+ {
+ _currentState.Pipeline.AlphaToCoverageEnable = multisample.AlphaToCoverageEnable;
+ _currentState.Pipeline.AlphaToOneEnable = multisample.AlphaToOneEnable;
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public void UpdateScissors(ReadOnlySpan> regions)
+ {
+ for (int i = 0; i < regions.Length; i++)
+ {
+ var region = regions[i];
+
+ _currentState.Scissors[i] = new MTLScissorRect
+ {
+ height = (ulong)region.Height,
+ width = (ulong)region.Width,
+ x = (ulong)region.X,
+ y = (ulong)region.Y
+ };
+ }
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetScissors(renderCommandEncoder);
+ return;
+ }
+
+ SignalDirty(DirtyFlags.Scissors);
+ }
+
+ public void UpdateViewports(ReadOnlySpan viewports)
+ {
+ static float Clamp(float value)
+ {
+ return Math.Clamp(value, 0f, 1f);
+ }
+
+ for (int i = 0; i < viewports.Length; i++)
+ {
+ var viewport = viewports[i];
+ // Y coordinate is inverted
+ _currentState.Viewports[i] = new MTLViewport
+ {
+ originX = viewport.Region.X,
+ originY = viewport.Region.Y + viewport.Region.Height,
+ width = viewport.Region.Width,
+ height = -viewport.Region.Height,
+ znear = Clamp(viewport.DepthNear),
+ zfar = Clamp(viewport.DepthFar)
+ };
+ }
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetViewports(renderCommandEncoder);
+ return;
+ }
+
+ SignalDirty(DirtyFlags.Viewports);
+ }
+
+ public readonly void UpdateVertexBuffers(ReadOnlySpan vertexBuffers)
+ {
+ for (int i = 0; i < Constants.MaxVertexBuffers; i++)
+ {
+ if (i < vertexBuffers.Length)
+ {
+ var vertexBuffer = vertexBuffers[i];
+
+ _currentState.VertexBuffers[i] = new VertexBufferState(
+ vertexBuffer.Buffer.Handle,
+ vertexBuffer.Buffer.Offset,
+ vertexBuffer.Buffer.Size,
+ vertexBuffer.Divisor,
+ vertexBuffer.Stride);
+ }
+ else
+ {
+ _currentState.VertexBuffers[i] = VertexBufferState.Null;
+ }
+ }
+
+ // Update the buffers on the pipeline
+ UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs);
+
+ SignalDirty(DirtyFlags.RenderPipeline);
+ }
+
+ public readonly void UpdateUniformBuffers(ReadOnlySpan buffers)
+ {
+ foreach (BufferAssignment assignment in buffers)
+ {
+ var buffer = assignment.Range;
+ int index = assignment.Binding;
+
+ Auto mtlBuffer = buffer.Handle == BufferHandle.Null
+ ? null
+ : _bufferManager.GetBuffer(buffer.Handle, buffer.Write);
+
+ _currentState.UniformBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer);
+ }
+
+ SignalDirty(DirtyFlags.Uniforms);
+ }
+
+ public readonly void UpdateStorageBuffers(ReadOnlySpan buffers)
+ {
+ foreach (BufferAssignment assignment in buffers)
+ {
+ var buffer = assignment.Range;
+ int index = assignment.Binding;
+
+ Auto mtlBuffer = buffer.Handle == BufferHandle.Null
+ ? null
+ : _bufferManager.GetBuffer(buffer.Handle, buffer.Write);
+
+ _currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer);
+ }
+
+ SignalDirty(DirtyFlags.Storages);
+ }
+
+ public readonly void UpdateStorageBuffers(int first, ReadOnlySpan> buffers)
+ {
+ for (int i = 0; i < buffers.Length; i++)
+ {
+ var mtlBuffer = buffers[i];
+ int index = first + i;
+
+ _currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer);
+ }
+
+ SignalDirty(DirtyFlags.Storages);
+ }
+
+ public void UpdateCullMode(bool enable, Face face)
+ {
+ var dirtyScissor = (face == Face.FrontAndBack) != _currentState.CullBoth;
+
+ _currentState.CullMode = enable ? face.Convert() : MTLCullMode.None;
+ _currentState.CullBoth = face == Face.FrontAndBack;
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetCullMode(renderCommandEncoder);
+ SetScissors(renderCommandEncoder);
+ return;
+ }
+
+ // Mark dirty
+ SignalDirty(DirtyFlags.CullMode);
+
+ if (dirtyScissor)
+ {
+ SignalDirty(DirtyFlags.Scissors);
+ }
+ }
+
+ public readonly void UpdateFrontFace(FrontFace frontFace)
+ {
+ _currentState.Winding = frontFace.Convert();
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetFrontFace(renderCommandEncoder);
+ return;
+ }
+
+ SignalDirty(DirtyFlags.FrontFace);
+ }
+
+ private readonly void UpdateStencilRefValue(int frontRef, int backRef)
+ {
+ _currentState.FrontRefValue = frontRef;
+ _currentState.BackRefValue = backRef;
+
+ // Inline update
+ if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder))
+ {
+ SetStencilRefValue(renderCommandEncoder);
+ }
+
+ SignalDirty(DirtyFlags.StencilRef);
+ }
+
+ public readonly void UpdateTextureAndSampler(ShaderStage stage, int binding, TextureBase texture, SamplerHolder samplerHolder)
+ {
+ if (texture != null)
+ {
+ _currentState.TextureRefs[binding] = new(stage, texture, samplerHolder?.GetSampler());
+ }
+ else
+ {
+ _currentState.TextureRefs[binding] = default;
+ }
+
+ SignalDirty(DirtyFlags.Textures);
+ }
+
+ public readonly void UpdateImage(ShaderStage stage, int binding, TextureBase image)
+ {
+ if (image is Texture view)
+ {
+ _currentState.ImageRefs[binding] = new(stage, view);
+ }
+ else
+ {
+ _currentState.ImageRefs[binding] = default;
+ }
+
+ SignalDirty(DirtyFlags.Images);
+ }
+
+ public readonly void UpdateTextureArray(ShaderStage stage, int binding, TextureArray array)
+ {
+ ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.TextureArrayRefs, binding, ArrayGrowthSize);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef = new EncoderState.ArrayRef(stage, array);
+
+ SignalDirty(DirtyFlags.Textures);
+ }
+ }
+
+ public readonly void UpdateTextureArraySeparate(ShaderStage stage, int setIndex, TextureArray array)
+ {
+ ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.TextureArrayExtraRefs, setIndex - MetalRenderer.TotalSets);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef = new EncoderState.ArrayRef(stage, array);
+
+ SignalDirty(DirtyFlags.Textures);
+ }
+ }
+
+ public readonly void UpdateImageArray(ShaderStage stage, int binding, ImageArray array)
+ {
+ ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.ImageArrayRefs, binding, ArrayGrowthSize);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef = new EncoderState.ArrayRef(stage, array);
+
+ SignalDirty(DirtyFlags.Images);
+ }
+ }
+
+ public readonly void UpdateImageArraySeparate(ShaderStage stage, int setIndex, ImageArray array)
+ {
+ ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.ImageArrayExtraRefs, setIndex - MetalRenderer.TotalSets);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef = new EncoderState.ArrayRef(stage, array);
+
+ SignalDirty(DirtyFlags.Images);
+ }
+ }
+
+ private static ref EncoderState.ArrayRef GetArrayRef(ref EncoderState.ArrayRef[] array, int index, int growthSize = 1)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+
+ if (array.Length <= index)
+ {
+ Array.Resize(ref array, index + growthSize);
+ }
+
+ return ref array[index];
+ }
+
+ private readonly void SetDepthStencilState(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ if (DepthStencil != null)
+ {
+ MTLDepthStencilState state = _depthStencilCache.GetOrCreate(_currentState.DepthStencilUid);
+
+ renderCommandEncoder.SetDepthStencilState(state);
+ }
+ else
+ {
+ renderCommandEncoder.SetDepthStencilState(_defaultState);
+ }
+ }
+
+ private readonly void SetDepthClamp(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ renderCommandEncoder.SetDepthClipMode(_currentState.DepthClipMode);
+ }
+
+ private readonly void SetDepthBias(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ renderCommandEncoder.SetDepthBias(_currentState.DepthBias, _currentState.SlopeScale, _currentState.Clamp);
+ }
+
+ private unsafe void SetScissors(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ var isTriangles = (_currentState.Topology == PrimitiveTopology.Triangles) ||
+ (_currentState.Topology == PrimitiveTopology.TriangleStrip);
+
+ if (_currentState.CullBoth && isTriangles)
+ {
+ renderCommandEncoder.SetScissorRect(new MTLScissorRect { x = 0, y = 0, width = 0, height = 0 });
+ }
+ else
+ {
+ if (_currentState.Scissors.Length > 0)
+ {
+ fixed (MTLScissorRect* pMtlScissors = _currentState.Scissors)
+ {
+ renderCommandEncoder.SetScissorRects((IntPtr)pMtlScissors, (ulong)_currentState.Scissors.Length);
+ }
+ }
+ }
+ }
+
+ private readonly unsafe void SetViewports(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ if (_currentState.Viewports.Length > 0)
+ {
+ fixed (MTLViewport* pMtlViewports = _currentState.Viewports)
+ {
+ renderCommandEncoder.SetViewports((IntPtr)pMtlViewports, (ulong)_currentState.Viewports.Length);
+ }
+ }
+ }
+
+ private readonly void UpdatePipelineVertexState(VertexBufferState[] bufferDescriptors, VertexAttribDescriptor[] attribDescriptors)
+ {
+ ref PipelineState pipeline = ref _currentState.Pipeline;
+ uint indexMask = 0;
+
+ for (int i = 0; i < attribDescriptors.Length; i++)
+ {
+ ref var attrib = ref pipeline.Internal.VertexAttributes[i];
+
+ if (attribDescriptors[i].IsZero)
+ {
+ attrib.Format = attribDescriptors[i].Format.Convert();
+ indexMask |= 1u << (int)Constants.ZeroBufferIndex;
+ attrib.BufferIndex = Constants.ZeroBufferIndex;
+ attrib.Offset = 0;
+ }
+ else
+ {
+ attrib.Format = attribDescriptors[i].Format.Convert();
+ indexMask |= 1u << attribDescriptors[i].BufferIndex;
+ attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex;
+ attrib.Offset = (ulong)attribDescriptors[i].Offset;
+ }
+ }
+
+ for (int i = 0; i < bufferDescriptors.Length; i++)
+ {
+ ref var layout = ref pipeline.Internal.VertexBindings[i];
+
+ if ((indexMask & (1u << i)) != 0)
+ {
+ layout.Stride = (uint)bufferDescriptors[i].Stride;
+
+ if (layout.Stride == 0)
+ {
+ layout.Stride = 1;
+ layout.StepFunction = MTLVertexStepFunction.Constant;
+ layout.StepRate = 0;
+ }
+ else
+ {
+ if (bufferDescriptors[i].Divisor > 0)
+ {
+ layout.StepFunction = MTLVertexStepFunction.PerInstance;
+ layout.StepRate = (uint)bufferDescriptors[i].Divisor;
+ }
+ else
+ {
+ layout.StepFunction = MTLVertexStepFunction.PerVertex;
+ layout.StepRate = 1;
+ }
+ }
+ }
+ else
+ {
+ layout = new();
+ }
+ }
+
+ ref var zeroBufLayout = ref pipeline.Internal.VertexBindings[(int)Constants.ZeroBufferIndex];
+
+ // Zero buffer
+ if ((indexMask & (1u << (int)Constants.ZeroBufferIndex)) != 0)
+ {
+ zeroBufLayout.Stride = 1;
+ zeroBufLayout.StepFunction = MTLVertexStepFunction.Constant;
+ zeroBufLayout.StepRate = 0;
+ }
+ else
+ {
+ zeroBufLayout = new();
+ }
+
+ pipeline.VertexAttributeDescriptionsCount = (uint)attribDescriptors.Length;
+ pipeline.VertexBindingDescriptionsCount = Constants.ZeroBufferIndex + 1; // TODO: move this out?
+ }
+
+ private readonly void SetVertexBuffers(VertexBufferState[] bufferStates, ref readonly RenderEncoderBindings bindings)
+ {
+ for (int i = 0; i < bufferStates.Length; i++)
+ {
+ (MTLBuffer mtlBuffer, int offset) = bufferStates[i].GetVertexBuffer(_bufferManager, _pipeline.Cbs);
+
+ if (mtlBuffer.NativePtr != IntPtr.Zero)
+ {
+ bindings.VertexBuffers.Add(new BufferResource(mtlBuffer, (ulong)offset, (ulong)i));
+ }
+ }
+
+ Auto autoZeroBuffer = _zeroBuffer == BufferHandle.Null
+ ? null
+ : _bufferManager.GetBuffer(_zeroBuffer, false);
+
+ if (autoZeroBuffer == null)
+ {
+ return;
+ }
+
+ var zeroMtlBuffer = autoZeroBuffer.Get(_pipeline.Cbs).Value;
+ bindings.VertexBuffers.Add(new BufferResource(zeroMtlBuffer, 0, Constants.ZeroBufferIndex));
+ }
+
+ private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForBuffer(ref BufferRef buffer)
+ {
+ ulong gpuAddress = 0;
+ IntPtr nativePtr = IntPtr.Zero;
+
+ var range = buffer.Range;
+ var autoBuffer = buffer.Buffer;
+
+ if (autoBuffer != null)
+ {
+ var offset = 0;
+ MTLBuffer mtlBuffer;
+
+ if (range.HasValue)
+ {
+ offset = range.Value.Offset;
+ mtlBuffer = autoBuffer.Get(_pipeline.Cbs, offset, range.Value.Size, range.Value.Write).Value;
+ }
+ else
+ {
+ mtlBuffer = autoBuffer.Get(_pipeline.Cbs).Value;
+ }
+
+ gpuAddress = mtlBuffer.GpuAddress + (ulong)offset;
+ nativePtr = mtlBuffer.NativePtr;
+ }
+
+ return (gpuAddress, nativePtr);
+ }
+
+ private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForTexture(ref TextureRef texture)
+ {
+ var storage = texture.Storage;
+
+ ulong gpuAddress = 0;
+ IntPtr nativePtr = IntPtr.Zero;
+
+ if (storage != null)
+ {
+ if (storage is TextureBuffer textureBuffer)
+ {
+ textureBuffer.RebuildStorage(false);
+ }
+
+ var mtlTexture = storage.GetHandle();
+
+ gpuAddress = mtlTexture.GpuResourceID._impl;
+ nativePtr = mtlTexture.NativePtr;
+ }
+
+ return (gpuAddress, nativePtr);
+ }
+
+ private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForImage(ref ImageRef image)
+ {
+ var storage = image.Storage;
+
+ ulong gpuAddress = 0;
+ IntPtr nativePtr = IntPtr.Zero;
+
+ if (storage != null)
+ {
+ var mtlTexture = storage.GetHandle();
+
+ gpuAddress = mtlTexture.GpuResourceID._impl;
+ nativePtr = mtlTexture.NativePtr;
+ }
+
+ return (gpuAddress, nativePtr);
+ }
+
+ private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForTextureBuffer(ref TextureBuffer bufferTexture)
+ {
+ ulong gpuAddress = 0;
+ IntPtr nativePtr = IntPtr.Zero;
+
+ if (bufferTexture != null)
+ {
+ bufferTexture.RebuildStorage(false);
+
+ var mtlTexture = bufferTexture.GetHandle();
+
+ gpuAddress = mtlTexture.GpuResourceID._impl;
+ nativePtr = mtlTexture.NativePtr;
+ }
+
+ return (gpuAddress, nativePtr);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void AddResource(IntPtr resourcePointer, MTLResourceUsage usage, MTLRenderStages stages, ref readonly RenderEncoderBindings bindings)
+ {
+ if (resourcePointer != IntPtr.Zero)
+ {
+ bindings.Resources.Add(new Resource(new MTLResource(resourcePointer), usage, stages));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void AddResource(IntPtr resourcePointer, MTLResourceUsage usage, ref readonly ComputeEncoderBindings bindings)
+ {
+ if (resourcePointer != IntPtr.Zero)
+ {
+ bindings.Resources.Add(new Resource(new MTLResource(resourcePointer), usage, 0));
+ }
+ }
+
+ private readonly void UpdateAndBind(Program program, uint setIndex, ref readonly RenderEncoderBindings bindings)
+ {
+ var bindingSegments = program.BindingSegments[setIndex];
+
+ if (bindingSegments.Length == 0)
+ {
+ return;
+ }
+
+ ScopedTemporaryBuffer vertArgBuffer = default;
+ ScopedTemporaryBuffer fragArgBuffer = default;
+
+ if (program.ArgumentBufferSizes[setIndex] > 0)
+ {
+ vertArgBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.ArgumentBufferSizes[setIndex] * sizeof(ulong));
+ }
+
+ if (program.FragArgumentBufferSizes[setIndex] > 0)
+ {
+ fragArgBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.FragArgumentBufferSizes[setIndex] * sizeof(ulong));
+ }
+
+ Span vertResourceIds = stackalloc ulong[program.ArgumentBufferSizes[setIndex]];
+ Span fragResourceIds = stackalloc ulong[program.FragArgumentBufferSizes[setIndex]];
+
+ var vertResourceIdIndex = 0;
+ var fragResourceIdIndex = 0;
+
+ foreach (ResourceBindingSegment segment in bindingSegments)
+ {
+ int binding = segment.Binding;
+ int count = segment.Count;
+
+ switch (setIndex)
+ {
+ case Constants.ConstantBuffersSetIndex:
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref BufferRef buffer = ref _currentState.UniformBufferRefs[index];
+ var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings);
+ }
+ break;
+ case Constants.StorageBuffersSetIndex:
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref BufferRef buffer = ref _currentState.StorageBufferRefs[index];
+ var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings);
+ }
+ break;
+ case Constants.TexturesSetIndex:
+ if (!segment.IsArray)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref var texture = ref _currentState.TextureRefs[index];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref texture);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+
+ if (texture.Sampler != null)
+ {
+ vertResourceIds[vertResourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl;
+ vertResourceIdIndex++;
+ }
+
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+
+ if (texture.Sampler != null)
+ {
+ fragResourceIds[fragResourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl;
+ fragResourceIdIndex++;
+ }
+
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings);
+ }
+ }
+ else
+ {
+ var textureArray = _currentState.TextureArrayRefs[binding].Array;
+
+ if (segment.Type != ResourceType.BufferTexture)
+ {
+ var textures = textureArray.GetTextureRefs();
+ var samplers = new Auto[textures.Length];
+
+ for (int i = 0; i < textures.Length; i++)
+ {
+ TextureRef texture = textures[i];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref texture);
+
+ samplers[i] = texture.Sampler;
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings);
+ }
+
+ foreach (var sampler in samplers)
+ {
+ ulong gpuAddress = 0;
+
+ if (sampler != null)
+ {
+ gpuAddress = sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl;
+ }
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+ }
+ }
+ }
+ else
+ {
+ var bufferTextures = textureArray.GetBufferTextureRefs();
+
+ for (int i = 0; i < bufferTextures.Length; i++)
+ {
+ TextureBuffer bufferTexture = bufferTextures[i];
+ var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref bufferTexture);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings);
+ }
+ }
+ }
+ break;
+ case Constants.ImagesSetIndex:
+ if (!segment.IsArray)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref var image = ref _currentState.ImageRefs[index];
+ var (gpuAddress, nativePtr) = AddressForImage(ref image);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings);
+ }
+ }
+ else
+ {
+ var imageArray = _currentState.ImageArrayRefs[binding].Array;
+
+ if (segment.Type != ResourceType.BufferImage)
+ {
+ var images = imageArray.GetTextureRefs();
+
+ for (int i = 0; i < images.Length; i++)
+ {
+ TextureRef image = images[i];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref image);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings);
+ }
+ }
+ else
+ {
+ var bufferImages = imageArray.GetBufferTextureRefs();
+
+ for (int i = 0; i < bufferImages.Length; i++)
+ {
+ TextureBuffer image = bufferImages[i];
+ var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref image);
+
+ MTLRenderStages renderStages = 0;
+
+ if ((segment.Stages & ResourceStages.Vertex) != 0)
+ {
+ vertResourceIds[vertResourceIdIndex] = gpuAddress;
+ vertResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageVertex;
+ }
+
+ if ((segment.Stages & ResourceStages.Fragment) != 0)
+ {
+ fragResourceIds[fragResourceIdIndex] = gpuAddress;
+ fragResourceIdIndex++;
+ renderStages |= MTLRenderStages.RenderStageFragment;
+ }
+
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (program.ArgumentBufferSizes[setIndex] > 0)
+ {
+ vertArgBuffer.Holder.SetDataUnchecked(vertArgBuffer.Offset, MemoryMarshal.AsBytes(vertResourceIds));
+ var mtlVertArgBuffer = _bufferManager.GetBuffer(vertArgBuffer.Handle, false).Get(_pipeline.Cbs).Value;
+ bindings.VertexBuffers.Add(new BufferResource(mtlVertArgBuffer, (uint)vertArgBuffer.Range.Offset, SetIndexToBindingIndex(setIndex)));
+ }
+
+ if (program.FragArgumentBufferSizes[setIndex] > 0)
+ {
+ fragArgBuffer.Holder.SetDataUnchecked(fragArgBuffer.Offset, MemoryMarshal.AsBytes(fragResourceIds));
+ var mtlFragArgBuffer = _bufferManager.GetBuffer(fragArgBuffer.Handle, false).Get(_pipeline.Cbs).Value;
+ bindings.FragmentBuffers.Add(new BufferResource(mtlFragArgBuffer, (uint)fragArgBuffer.Range.Offset, SetIndexToBindingIndex(setIndex)));
+ }
+ }
+
+ private readonly void UpdateAndBind(Program program, uint setIndex, ref readonly ComputeEncoderBindings bindings)
+ {
+ var bindingSegments = program.BindingSegments[setIndex];
+
+ if (bindingSegments.Length == 0)
+ {
+ return;
+ }
+
+ ScopedTemporaryBuffer argBuffer = default;
+
+ if (program.ArgumentBufferSizes[setIndex] > 0)
+ {
+ argBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.ArgumentBufferSizes[setIndex] * sizeof(ulong));
+ }
+
+ Span resourceIds = stackalloc ulong[program.ArgumentBufferSizes[setIndex]];
+ var resourceIdIndex = 0;
+
+ foreach (ResourceBindingSegment segment in bindingSegments)
+ {
+ int binding = segment.Binding;
+ int count = segment.Count;
+
+ switch (setIndex)
+ {
+ case Constants.ConstantBuffersSetIndex:
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref BufferRef buffer = ref _currentState.UniformBufferRefs[index];
+ var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read, in bindings);
+ bindings.Resources.Add(new Resource(new MTLResource(nativePtr), MTLResourceUsage.Read, 0));
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ break;
+ case Constants.StorageBuffersSetIndex:
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref BufferRef buffer = ref _currentState.StorageBufferRefs[index];
+ var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ break;
+ case Constants.TexturesSetIndex:
+ if (!segment.IsArray)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref var texture = ref _currentState.TextureRefs[index];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref texture);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+
+ if (texture.Sampler != null)
+ {
+ resourceIds[resourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl;
+ resourceIdIndex++;
+ }
+ }
+ }
+ }
+ else
+ {
+ var textureArray = _currentState.TextureArrayRefs[binding].Array;
+
+ if (segment.Type != ResourceType.BufferTexture)
+ {
+ var textures = textureArray.GetTextureRefs();
+ var samplers = new Auto[textures.Length];
+
+ for (int i = 0; i < textures.Length; i++)
+ {
+ TextureRef texture = textures[i];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref texture);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+
+ samplers[i] = texture.Sampler;
+ }
+ }
+
+ foreach (var sampler in samplers)
+ {
+ if (sampler != null)
+ {
+ resourceIds[resourceIdIndex] = sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl;
+ resourceIdIndex++;
+ }
+ }
+ }
+ else
+ {
+ var bufferTextures = textureArray.GetBufferTextureRefs();
+
+ for (int i = 0; i < bufferTextures.Length; i++)
+ {
+ TextureBuffer bufferTexture = bufferTextures[i];
+ var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref bufferTexture);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ }
+ }
+ break;
+ case Constants.ImagesSetIndex:
+ if (!segment.IsArray)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ int index = binding + i;
+
+ ref var image = ref _currentState.ImageRefs[index];
+ var (gpuAddress, nativePtr) = AddressForImage(ref image);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ }
+ else
+ {
+ var imageArray = _currentState.ImageArrayRefs[binding].Array;
+
+ if (segment.Type != ResourceType.BufferImage)
+ {
+ var images = imageArray.GetTextureRefs();
+
+ for (int i = 0; i < images.Length; i++)
+ {
+ TextureRef image = images[i];
+ var (gpuAddress, nativePtr) = AddressForTexture(ref image);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ }
+ else
+ {
+ var bufferImages = imageArray.GetBufferTextureRefs();
+
+ for (int i = 0; i < bufferImages.Length; i++)
+ {
+ TextureBuffer image = bufferImages[i];
+ var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref image);
+
+ if ((segment.Stages & ResourceStages.Compute) != 0)
+ {
+ AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings);
+ resourceIds[resourceIdIndex] = gpuAddress;
+ resourceIdIndex++;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (program.ArgumentBufferSizes[setIndex] > 0)
+ {
+ argBuffer.Holder.SetDataUnchecked(argBuffer.Offset, MemoryMarshal.AsBytes(resourceIds));
+ var mtlArgBuffer = _bufferManager.GetBuffer(argBuffer.Handle, false).Get(_pipeline.Cbs).Value;
+ bindings.Buffers.Add(new BufferResource(mtlArgBuffer, (uint)argBuffer.Range.Offset, SetIndexToBindingIndex(setIndex)));
+ }
+ }
+
+ private static uint SetIndexToBindingIndex(uint setIndex)
+ {
+ return setIndex switch
+ {
+ Constants.ConstantBuffersSetIndex => Constants.ConstantBuffersIndex,
+ Constants.StorageBuffersSetIndex => Constants.StorageBuffersIndex,
+ Constants.TexturesSetIndex => Constants.TexturesIndex,
+ Constants.ImagesSetIndex => Constants.ImagesIndex,
+ _ => throw new NotImplementedException()
+ };
+ }
+
+ private readonly void SetCullMode(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ renderCommandEncoder.SetCullMode(_currentState.CullMode);
+ }
+
+ private readonly void SetFrontFace(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ renderCommandEncoder.SetFrontFacingWinding(_currentState.Winding);
+ }
+
+ private readonly void SetStencilRefValue(MTLRenderCommandEncoder renderCommandEncoder)
+ {
+ renderCommandEncoder.SetStencilReferenceValues((uint)_currentState.FrontRefValue, (uint)_currentState.BackRefValue);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/EnumConversion.cs b/src/Ryujinx.Graphics.Metal/EnumConversion.cs
new file mode 100644
index 000000000..e498546e8
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/EnumConversion.cs
@@ -0,0 +1,293 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ static class EnumConversion
+ {
+ public static MTLSamplerAddressMode Convert(this AddressMode mode)
+ {
+ return mode switch
+ {
+ AddressMode.Clamp => MTLSamplerAddressMode.ClampToEdge, // TODO: Should be clamp.
+ AddressMode.Repeat => MTLSamplerAddressMode.Repeat,
+ AddressMode.MirrorClamp => MTLSamplerAddressMode.MirrorClampToEdge, // TODO: Should be mirror clamp.
+ AddressMode.MirroredRepeat => MTLSamplerAddressMode.MirrorRepeat,
+ AddressMode.ClampToBorder => MTLSamplerAddressMode.ClampToBorderColor,
+ AddressMode.ClampToEdge => MTLSamplerAddressMode.ClampToEdge,
+ AddressMode.MirrorClampToEdge => MTLSamplerAddressMode.MirrorClampToEdge,
+ AddressMode.MirrorClampToBorder => MTLSamplerAddressMode.ClampToBorderColor, // TODO: Should be mirror clamp to border.
+ _ => LogInvalidAndReturn(mode, nameof(AddressMode), MTLSamplerAddressMode.ClampToEdge) // TODO: Should be clamp.
+ };
+ }
+
+ public static MTLBlendFactor Convert(this BlendFactor factor)
+ {
+ return factor switch
+ {
+ BlendFactor.Zero or BlendFactor.ZeroGl => MTLBlendFactor.Zero,
+ BlendFactor.One or BlendFactor.OneGl => MTLBlendFactor.One,
+ BlendFactor.SrcColor or BlendFactor.SrcColorGl => MTLBlendFactor.SourceColor,
+ BlendFactor.OneMinusSrcColor or BlendFactor.OneMinusSrcColorGl => MTLBlendFactor.OneMinusSourceColor,
+ BlendFactor.SrcAlpha or BlendFactor.SrcAlphaGl => MTLBlendFactor.SourceAlpha,
+ BlendFactor.OneMinusSrcAlpha or BlendFactor.OneMinusSrcAlphaGl => MTLBlendFactor.OneMinusSourceAlpha,
+ BlendFactor.DstAlpha or BlendFactor.DstAlphaGl => MTLBlendFactor.DestinationAlpha,
+ BlendFactor.OneMinusDstAlpha or BlendFactor.OneMinusDstAlphaGl => MTLBlendFactor.OneMinusDestinationAlpha,
+ BlendFactor.DstColor or BlendFactor.DstColorGl => MTLBlendFactor.DestinationColor,
+ BlendFactor.OneMinusDstColor or BlendFactor.OneMinusDstColorGl => MTLBlendFactor.OneMinusDestinationColor,
+ BlendFactor.SrcAlphaSaturate or BlendFactor.SrcAlphaSaturateGl => MTLBlendFactor.SourceAlphaSaturated,
+ BlendFactor.Src1Color or BlendFactor.Src1ColorGl => MTLBlendFactor.Source1Color,
+ BlendFactor.OneMinusSrc1Color or BlendFactor.OneMinusSrc1ColorGl => MTLBlendFactor.OneMinusSource1Color,
+ BlendFactor.Src1Alpha or BlendFactor.Src1AlphaGl => MTLBlendFactor.Source1Alpha,
+ BlendFactor.OneMinusSrc1Alpha or BlendFactor.OneMinusSrc1AlphaGl => MTLBlendFactor.OneMinusSource1Alpha,
+ BlendFactor.ConstantColor => MTLBlendFactor.BlendColor,
+ BlendFactor.OneMinusConstantColor => MTLBlendFactor.OneMinusBlendColor,
+ BlendFactor.ConstantAlpha => MTLBlendFactor.BlendAlpha,
+ BlendFactor.OneMinusConstantAlpha => MTLBlendFactor.OneMinusBlendAlpha,
+ _ => LogInvalidAndReturn(factor, nameof(BlendFactor), MTLBlendFactor.Zero)
+ };
+ }
+
+ public static MTLBlendOperation Convert(this BlendOp op)
+ {
+ return op switch
+ {
+ BlendOp.Add or BlendOp.AddGl => MTLBlendOperation.Add,
+ BlendOp.Subtract or BlendOp.SubtractGl => MTLBlendOperation.Subtract,
+ BlendOp.ReverseSubtract or BlendOp.ReverseSubtractGl => MTLBlendOperation.ReverseSubtract,
+ BlendOp.Minimum => MTLBlendOperation.Min,
+ BlendOp.Maximum => MTLBlendOperation.Max,
+ _ => LogInvalidAndReturn(op, nameof(BlendOp), MTLBlendOperation.Add)
+ };
+ }
+
+ public static MTLCompareFunction Convert(this CompareOp op)
+ {
+ return op switch
+ {
+ CompareOp.Never or CompareOp.NeverGl => MTLCompareFunction.Never,
+ CompareOp.Less or CompareOp.LessGl => MTLCompareFunction.Less,
+ CompareOp.Equal or CompareOp.EqualGl => MTLCompareFunction.Equal,
+ CompareOp.LessOrEqual or CompareOp.LessOrEqualGl => MTLCompareFunction.LessEqual,
+ CompareOp.Greater or CompareOp.GreaterGl => MTLCompareFunction.Greater,
+ CompareOp.NotEqual or CompareOp.NotEqualGl => MTLCompareFunction.NotEqual,
+ CompareOp.GreaterOrEqual or CompareOp.GreaterOrEqualGl => MTLCompareFunction.GreaterEqual,
+ CompareOp.Always or CompareOp.AlwaysGl => MTLCompareFunction.Always,
+ _ => LogInvalidAndReturn(op, nameof(CompareOp), MTLCompareFunction.Never)
+ };
+ }
+
+ public static MTLCullMode Convert(this Face face)
+ {
+ return face switch
+ {
+ Face.Back => MTLCullMode.Back,
+ Face.Front => MTLCullMode.Front,
+ Face.FrontAndBack => MTLCullMode.None,
+ _ => LogInvalidAndReturn(face, nameof(Face), MTLCullMode.Back)
+ };
+ }
+
+ public static MTLWinding Convert(this FrontFace frontFace)
+ {
+ // The viewport is flipped vertically, therefore we need to switch the winding order as well
+ return frontFace switch
+ {
+ FrontFace.Clockwise => MTLWinding.CounterClockwise,
+ FrontFace.CounterClockwise => MTLWinding.Clockwise,
+ _ => LogInvalidAndReturn(frontFace, nameof(FrontFace), MTLWinding.Clockwise)
+ };
+ }
+
+ public static MTLIndexType Convert(this IndexType type)
+ {
+ return type switch
+ {
+ IndexType.UShort => MTLIndexType.UInt16,
+ IndexType.UInt => MTLIndexType.UInt32,
+ _ => LogInvalidAndReturn(type, nameof(IndexType), MTLIndexType.UInt16)
+ };
+ }
+
+ public static MTLLogicOperation Convert(this LogicalOp op)
+ {
+ return op switch
+ {
+ LogicalOp.Clear => MTLLogicOperation.Clear,
+ LogicalOp.And => MTLLogicOperation.And,
+ LogicalOp.AndReverse => MTLLogicOperation.AndReverse,
+ LogicalOp.Copy => MTLLogicOperation.Copy,
+ LogicalOp.AndInverted => MTLLogicOperation.AndInverted,
+ LogicalOp.Noop => MTLLogicOperation.Noop,
+ LogicalOp.Xor => MTLLogicOperation.Xor,
+ LogicalOp.Or => MTLLogicOperation.Or,
+ LogicalOp.Nor => MTLLogicOperation.Nor,
+ LogicalOp.Equiv => MTLLogicOperation.Equivalence,
+ LogicalOp.Invert => MTLLogicOperation.Invert,
+ LogicalOp.OrReverse => MTLLogicOperation.OrReverse,
+ LogicalOp.CopyInverted => MTLLogicOperation.CopyInverted,
+ LogicalOp.OrInverted => MTLLogicOperation.OrInverted,
+ LogicalOp.Nand => MTLLogicOperation.Nand,
+ LogicalOp.Set => MTLLogicOperation.Set,
+ _ => LogInvalidAndReturn(op, nameof(LogicalOp), MTLLogicOperation.And)
+ };
+ }
+
+ public static MTLSamplerMinMagFilter Convert(this MagFilter filter)
+ {
+ return filter switch
+ {
+ MagFilter.Nearest => MTLSamplerMinMagFilter.Nearest,
+ MagFilter.Linear => MTLSamplerMinMagFilter.Linear,
+ _ => LogInvalidAndReturn(filter, nameof(MagFilter), MTLSamplerMinMagFilter.Nearest)
+ };
+ }
+
+ public static (MTLSamplerMinMagFilter, MTLSamplerMipFilter) Convert(this MinFilter filter)
+ {
+ return filter switch
+ {
+ MinFilter.Nearest => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest),
+ MinFilter.Linear => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Linear),
+ MinFilter.NearestMipmapNearest => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest),
+ MinFilter.LinearMipmapNearest => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Nearest),
+ MinFilter.NearestMipmapLinear => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Linear),
+ MinFilter.LinearMipmapLinear => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Linear),
+ _ => LogInvalidAndReturn(filter, nameof(MinFilter), (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest))
+
+ };
+ }
+
+ public static MTLPrimitiveType Convert(this PrimitiveTopology topology)
+ {
+ return topology switch
+ {
+ PrimitiveTopology.Points => MTLPrimitiveType.Point,
+ PrimitiveTopology.Lines => MTLPrimitiveType.Line,
+ PrimitiveTopology.LineStrip => MTLPrimitiveType.LineStrip,
+ PrimitiveTopology.Triangles => MTLPrimitiveType.Triangle,
+ PrimitiveTopology.TriangleStrip => MTLPrimitiveType.TriangleStrip,
+ _ => LogInvalidAndReturn(topology, nameof(PrimitiveTopology), MTLPrimitiveType.Triangle)
+ };
+ }
+
+ public static MTLStencilOperation Convert(this StencilOp op)
+ {
+ return op switch
+ {
+ StencilOp.Keep or StencilOp.KeepGl => MTLStencilOperation.Keep,
+ StencilOp.Zero or StencilOp.ZeroGl => MTLStencilOperation.Zero,
+ StencilOp.Replace or StencilOp.ReplaceGl => MTLStencilOperation.Replace,
+ StencilOp.IncrementAndClamp or StencilOp.IncrementAndClampGl => MTLStencilOperation.IncrementClamp,
+ StencilOp.DecrementAndClamp or StencilOp.DecrementAndClampGl => MTLStencilOperation.DecrementClamp,
+ StencilOp.Invert or StencilOp.InvertGl => MTLStencilOperation.Invert,
+ StencilOp.IncrementAndWrap or StencilOp.IncrementAndWrapGl => MTLStencilOperation.IncrementWrap,
+ StencilOp.DecrementAndWrap or StencilOp.DecrementAndWrapGl => MTLStencilOperation.DecrementWrap,
+ _ => LogInvalidAndReturn(op, nameof(StencilOp), MTLStencilOperation.Keep)
+ };
+ }
+
+ public static MTLTextureType Convert(this Target target)
+ {
+ return target switch
+ {
+ Target.TextureBuffer => MTLTextureType.TextureBuffer,
+ Target.Texture1D => MTLTextureType.Type1D,
+ Target.Texture1DArray => MTLTextureType.Type1DArray,
+ Target.Texture2D => MTLTextureType.Type2D,
+ Target.Texture2DArray => MTLTextureType.Type2DArray,
+ Target.Texture2DMultisample => MTLTextureType.Type2DMultisample,
+ Target.Texture2DMultisampleArray => MTLTextureType.Type2DMultisampleArray,
+ Target.Texture3D => MTLTextureType.Type3D,
+ Target.Cubemap => MTLTextureType.Cube,
+ Target.CubemapArray => MTLTextureType.CubeArray,
+ _ => LogInvalidAndReturn(target, nameof(Target), MTLTextureType.Type2D)
+ };
+ }
+
+ public static MTLTextureSwizzle Convert(this SwizzleComponent swizzleComponent)
+ {
+ return swizzleComponent switch
+ {
+ SwizzleComponent.Zero => MTLTextureSwizzle.Zero,
+ SwizzleComponent.One => MTLTextureSwizzle.One,
+ SwizzleComponent.Red => MTLTextureSwizzle.Red,
+ SwizzleComponent.Green => MTLTextureSwizzle.Green,
+ SwizzleComponent.Blue => MTLTextureSwizzle.Blue,
+ SwizzleComponent.Alpha => MTLTextureSwizzle.Alpha,
+ _ => LogInvalidAndReturn(swizzleComponent, nameof(SwizzleComponent), MTLTextureSwizzle.Zero)
+ };
+ }
+
+ public static MTLVertexFormat Convert(this Format format)
+ {
+ return format switch
+ {
+ Format.R16Float => MTLVertexFormat.Half,
+ Format.R16G16Float => MTLVertexFormat.Half2,
+ Format.R16G16B16Float => MTLVertexFormat.Half3,
+ Format.R16G16B16A16Float => MTLVertexFormat.Half4,
+ Format.R32Float => MTLVertexFormat.Float,
+ Format.R32G32Float => MTLVertexFormat.Float2,
+ Format.R32G32B32Float => MTLVertexFormat.Float3,
+ Format.R11G11B10Float => MTLVertexFormat.FloatRG11B10,
+ Format.R32G32B32A32Float => MTLVertexFormat.Float4,
+ Format.R8Uint => MTLVertexFormat.UChar,
+ Format.R8G8Uint => MTLVertexFormat.UChar2,
+ Format.R8G8B8Uint => MTLVertexFormat.UChar3,
+ Format.R8G8B8A8Uint => MTLVertexFormat.UChar4,
+ Format.R16Uint => MTLVertexFormat.UShort,
+ Format.R16G16Uint => MTLVertexFormat.UShort2,
+ Format.R16G16B16Uint => MTLVertexFormat.UShort3,
+ Format.R16G16B16A16Uint => MTLVertexFormat.UShort4,
+ Format.R32Uint => MTLVertexFormat.UInt,
+ Format.R32G32Uint => MTLVertexFormat.UInt2,
+ Format.R32G32B32Uint => MTLVertexFormat.UInt3,
+ Format.R32G32B32A32Uint => MTLVertexFormat.UInt4,
+ Format.R8Sint => MTLVertexFormat.Char,
+ Format.R8G8Sint => MTLVertexFormat.Char2,
+ Format.R8G8B8Sint => MTLVertexFormat.Char3,
+ Format.R8G8B8A8Sint => MTLVertexFormat.Char4,
+ Format.R16Sint => MTLVertexFormat.Short,
+ Format.R16G16Sint => MTLVertexFormat.Short2,
+ Format.R16G16B16Sint => MTLVertexFormat.Short3,
+ Format.R16G16B16A16Sint => MTLVertexFormat.Short4,
+ Format.R32Sint => MTLVertexFormat.Int,
+ Format.R32G32Sint => MTLVertexFormat.Int2,
+ Format.R32G32B32Sint => MTLVertexFormat.Int3,
+ Format.R32G32B32A32Sint => MTLVertexFormat.Int4,
+ Format.R8Unorm => MTLVertexFormat.UCharNormalized,
+ Format.R8G8Unorm => MTLVertexFormat.UChar2Normalized,
+ Format.R8G8B8Unorm => MTLVertexFormat.UChar3Normalized,
+ Format.R8G8B8A8Unorm => MTLVertexFormat.UChar4Normalized,
+ Format.R16Unorm => MTLVertexFormat.UShortNormalized,
+ Format.R16G16Unorm => MTLVertexFormat.UShort2Normalized,
+ Format.R16G16B16Unorm => MTLVertexFormat.UShort3Normalized,
+ Format.R16G16B16A16Unorm => MTLVertexFormat.UShort4Normalized,
+ Format.R10G10B10A2Unorm => MTLVertexFormat.UInt1010102Normalized,
+ Format.R8Snorm => MTLVertexFormat.CharNormalized,
+ Format.R8G8Snorm => MTLVertexFormat.Char2Normalized,
+ Format.R8G8B8Snorm => MTLVertexFormat.Char3Normalized,
+ Format.R8G8B8A8Snorm => MTLVertexFormat.Char4Normalized,
+ Format.R16Snorm => MTLVertexFormat.ShortNormalized,
+ Format.R16G16Snorm => MTLVertexFormat.Short2Normalized,
+ Format.R16G16B16Snorm => MTLVertexFormat.Short3Normalized,
+ Format.R16G16B16A16Snorm => MTLVertexFormat.Short4Normalized,
+ Format.R10G10B10A2Snorm => MTLVertexFormat.Int1010102Normalized,
+
+ _ => LogInvalidAndReturn(format, nameof(Format), MTLVertexFormat.Float4)
+ };
+ }
+
+ private static T2 LogInvalidAndReturn(T1 value, string name, T2 defaultValue = default)
+ {
+ Logger.Debug?.Print(LogClass.Gpu, $"Invalid {name} enum value: {value}.");
+
+ return defaultValue;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/FenceHolder.cs b/src/Ryujinx.Graphics.Metal/FenceHolder.cs
new file mode 100644
index 000000000..a8dd28c0d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/FenceHolder.cs
@@ -0,0 +1,77 @@
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+using System.Threading;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class FenceHolder : IDisposable
+ {
+ private MTLCommandBuffer _fence;
+ private int _referenceCount;
+ private bool _disposed;
+
+ public FenceHolder(MTLCommandBuffer fence)
+ {
+ _fence = fence;
+ _referenceCount = 1;
+ }
+
+ public MTLCommandBuffer GetUnsafe()
+ {
+ return _fence;
+ }
+
+ public bool TryGet(out MTLCommandBuffer fence)
+ {
+ int lastValue;
+ do
+ {
+ lastValue = _referenceCount;
+
+ if (lastValue == 0)
+ {
+ fence = default;
+ return false;
+ }
+ } while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue);
+
+ fence = _fence;
+ return true;
+ }
+
+ public MTLCommandBuffer Get()
+ {
+ Interlocked.Increment(ref _referenceCount);
+ return _fence;
+ }
+
+ public void Put()
+ {
+ if (Interlocked.Decrement(ref _referenceCount) == 0)
+ {
+ _fence = default;
+ }
+ }
+
+ public void Wait()
+ {
+ _fence.WaitUntilCompleted();
+ }
+
+ public bool IsSignaled()
+ {
+ return _fence.Status == MTLCommandBufferStatus.Completed;
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Put();
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/FormatConverter.cs b/src/Ryujinx.Graphics.Metal/FormatConverter.cs
new file mode 100644
index 000000000..e099187b8
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/FormatConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.Metal
+{
+ class FormatConverter
+ {
+ public static void ConvertD24S8ToD32FS8(Span output, ReadOnlySpan input)
+ {
+ const float UnormToFloat = 1f / 0xffffff;
+
+ Span outputUint = MemoryMarshal.Cast(output);
+ ReadOnlySpan inputUint = MemoryMarshal.Cast(input);
+
+ int i = 0;
+
+ for (; i < inputUint.Length; i++)
+ {
+ uint depthStencil = inputUint[i];
+ uint depth = depthStencil >> 8;
+ uint stencil = depthStencil & 0xff;
+
+ int j = i * 2;
+
+ outputUint[j] = (uint)BitConverter.SingleToInt32Bits(depth * UnormToFloat);
+ outputUint[j + 1] = stencil;
+ }
+ }
+
+ public static void ConvertD32FS8ToD24S8(Span output, ReadOnlySpan input)
+ {
+ Span outputUint = MemoryMarshal.Cast(output);
+ ReadOnlySpan inputUint = MemoryMarshal.Cast(input);
+
+ int i = 0;
+
+ for (; i < inputUint.Length; i += 2)
+ {
+ float depth = BitConverter.Int32BitsToSingle((int)inputUint[i]);
+ uint stencil = inputUint[i + 1];
+ uint depthStencil = (Math.Clamp((uint)(depth * 0xffffff), 0, 0xffffff) << 8) | (stencil & 0xff);
+
+ int j = i >> 1;
+
+ outputUint[j] = depthStencil;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/FormatTable.cs b/src/Ryujinx.Graphics.Metal/FormatTable.cs
new file mode 100644
index 000000000..c1f8923f9
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/FormatTable.cs
@@ -0,0 +1,196 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ static class FormatTable
+ {
+ private static readonly MTLPixelFormat[] _table;
+
+ static FormatTable()
+ {
+ _table = new MTLPixelFormat[Enum.GetNames(typeof(Format)).Length];
+
+ Add(Format.R8Unorm, MTLPixelFormat.R8Unorm);
+ Add(Format.R8Snorm, MTLPixelFormat.R8Snorm);
+ Add(Format.R8Uint, MTLPixelFormat.R8Uint);
+ Add(Format.R8Sint, MTLPixelFormat.R8Sint);
+ Add(Format.R16Float, MTLPixelFormat.R16Float);
+ Add(Format.R16Unorm, MTLPixelFormat.R16Unorm);
+ Add(Format.R16Snorm, MTLPixelFormat.R16Snorm);
+ Add(Format.R16Uint, MTLPixelFormat.R16Uint);
+ Add(Format.R16Sint, MTLPixelFormat.R16Sint);
+ Add(Format.R32Float, MTLPixelFormat.R32Float);
+ Add(Format.R32Uint, MTLPixelFormat.R32Uint);
+ Add(Format.R32Sint, MTLPixelFormat.R32Sint);
+ Add(Format.R8G8Unorm, MTLPixelFormat.RG8Unorm);
+ Add(Format.R8G8Snorm, MTLPixelFormat.RG8Snorm);
+ Add(Format.R8G8Uint, MTLPixelFormat.RG8Uint);
+ Add(Format.R8G8Sint, MTLPixelFormat.RG8Sint);
+ Add(Format.R16G16Float, MTLPixelFormat.RG16Float);
+ Add(Format.R16G16Unorm, MTLPixelFormat.RG16Unorm);
+ Add(Format.R16G16Snorm, MTLPixelFormat.RG16Snorm);
+ Add(Format.R16G16Uint, MTLPixelFormat.RG16Uint);
+ Add(Format.R16G16Sint, MTLPixelFormat.RG16Sint);
+ Add(Format.R32G32Float, MTLPixelFormat.RG32Float);
+ Add(Format.R32G32Uint, MTLPixelFormat.RG32Uint);
+ Add(Format.R32G32Sint, MTLPixelFormat.RG32Sint);
+ // Add(Format.R8G8B8Unorm, MTLPixelFormat.R8G8B8Unorm);
+ // Add(Format.R8G8B8Snorm, MTLPixelFormat.R8G8B8Snorm);
+ // Add(Format.R8G8B8Uint, MTLPixelFormat.R8G8B8Uint);
+ // Add(Format.R8G8B8Sint, MTLPixelFormat.R8G8B8Sint);
+ // Add(Format.R16G16B16Float, MTLPixelFormat.R16G16B16Float);
+ // Add(Format.R16G16B16Unorm, MTLPixelFormat.R16G16B16Unorm);
+ // Add(Format.R16G16B16Snorm, MTLPixelFormat.R16G16B16SNorm);
+ // Add(Format.R16G16B16Uint, MTLPixelFormat.R16G16B16Uint);
+ // Add(Format.R16G16B16Sint, MTLPixelFormat.R16G16B16Sint);
+ // Add(Format.R32G32B32Float, MTLPixelFormat.R32G32B32Sfloat);
+ // Add(Format.R32G32B32Uint, MTLPixelFormat.R32G32B32Uint);
+ // Add(Format.R32G32B32Sint, MTLPixelFormat.R32G32B32Sint);
+ Add(Format.R8G8B8A8Unorm, MTLPixelFormat.RGBA8Unorm);
+ Add(Format.R8G8B8A8Snorm, MTLPixelFormat.RGBA8Snorm);
+ Add(Format.R8G8B8A8Uint, MTLPixelFormat.RGBA8Uint);
+ Add(Format.R8G8B8A8Sint, MTLPixelFormat.RGBA8Sint);
+ Add(Format.R16G16B16A16Float, MTLPixelFormat.RGBA16Float);
+ Add(Format.R16G16B16A16Unorm, MTLPixelFormat.RGBA16Unorm);
+ Add(Format.R16G16B16A16Snorm, MTLPixelFormat.RGBA16Snorm);
+ Add(Format.R16G16B16A16Uint, MTLPixelFormat.RGBA16Uint);
+ Add(Format.R16G16B16A16Sint, MTLPixelFormat.RGBA16Sint);
+ Add(Format.R32G32B32A32Float, MTLPixelFormat.RGBA32Float);
+ Add(Format.R32G32B32A32Uint, MTLPixelFormat.RGBA32Uint);
+ Add(Format.R32G32B32A32Sint, MTLPixelFormat.RGBA32Sint);
+ Add(Format.S8Uint, MTLPixelFormat.Stencil8);
+ Add(Format.D16Unorm, MTLPixelFormat.Depth16Unorm);
+ Add(Format.S8UintD24Unorm, MTLPixelFormat.Depth24UnormStencil8);
+ Add(Format.X8UintD24Unorm, MTLPixelFormat.Depth24UnormStencil8);
+ Add(Format.D32Float, MTLPixelFormat.Depth32Float);
+ Add(Format.D24UnormS8Uint, MTLPixelFormat.Depth24UnormStencil8);
+ Add(Format.D32FloatS8Uint, MTLPixelFormat.Depth32FloatStencil8);
+ Add(Format.R8G8B8A8Srgb, MTLPixelFormat.RGBA8UnormsRGB);
+ // Add(Format.R4G4Unorm, MTLPixelFormat.R4G4Unorm);
+ Add(Format.R4G4B4A4Unorm, MTLPixelFormat.RGBA8Unorm);
+ // Add(Format.R5G5B5X1Unorm, MTLPixelFormat.R5G5B5X1Unorm);
+ Add(Format.R5G5B5A1Unorm, MTLPixelFormat.BGR5A1Unorm);
+ Add(Format.R5G6B5Unorm, MTLPixelFormat.B5G6R5Unorm);
+ Add(Format.R10G10B10A2Unorm, MTLPixelFormat.RGB10A2Unorm);
+ Add(Format.R10G10B10A2Uint, MTLPixelFormat.RGB10A2Uint);
+ Add(Format.R11G11B10Float, MTLPixelFormat.RG11B10Float);
+ Add(Format.R9G9B9E5Float, MTLPixelFormat.RGB9E5Float);
+ Add(Format.Bc1RgbaUnorm, MTLPixelFormat.BC1RGBA);
+ Add(Format.Bc2Unorm, MTLPixelFormat.BC2RGBA);
+ Add(Format.Bc3Unorm, MTLPixelFormat.BC3RGBA);
+ Add(Format.Bc1RgbaSrgb, MTLPixelFormat.BC1RGBAsRGB);
+ Add(Format.Bc2Srgb, MTLPixelFormat.BC2RGBAsRGB);
+ Add(Format.Bc3Srgb, MTLPixelFormat.BC3RGBAsRGB);
+ Add(Format.Bc4Unorm, MTLPixelFormat.BC4RUnorm);
+ Add(Format.Bc4Snorm, MTLPixelFormat.BC4RSnorm);
+ Add(Format.Bc5Unorm, MTLPixelFormat.BC5RGUnorm);
+ Add(Format.Bc5Snorm, MTLPixelFormat.BC5RGSnorm);
+ Add(Format.Bc7Unorm, MTLPixelFormat.BC7RGBAUnorm);
+ Add(Format.Bc7Srgb, MTLPixelFormat.BC7RGBAUnormsRGB);
+ Add(Format.Bc6HSfloat, MTLPixelFormat.BC6HRGBFloat);
+ Add(Format.Bc6HUfloat, MTLPixelFormat.BC6HRGBUfloat);
+ Add(Format.Etc2RgbUnorm, MTLPixelFormat.ETC2RGB8);
+ // Add(Format.Etc2RgbaUnorm, MTLPixelFormat.ETC2RGBA8);
+ Add(Format.Etc2RgbPtaUnorm, MTLPixelFormat.ETC2RGB8A1);
+ Add(Format.Etc2RgbSrgb, MTLPixelFormat.ETC2RGB8sRGB);
+ // Add(Format.Etc2RgbaSrgb, MTLPixelFormat.ETC2RGBA8sRGB);
+ Add(Format.Etc2RgbPtaSrgb, MTLPixelFormat.ETC2RGB8A1sRGB);
+ // Add(Format.R8Uscaled, MTLPixelFormat.R8Uscaled);
+ // Add(Format.R8Sscaled, MTLPixelFormat.R8Sscaled);
+ // Add(Format.R16Uscaled, MTLPixelFormat.R16Uscaled);
+ // Add(Format.R16Sscaled, MTLPixelFormat.R16Sscaled);
+ // Add(Format.R32Uscaled, MTLPixelFormat.R32Uscaled);
+ // Add(Format.R32Sscaled, MTLPixelFormat.R32Sscaled);
+ // Add(Format.R8G8Uscaled, MTLPixelFormat.R8G8Uscaled);
+ // Add(Format.R8G8Sscaled, MTLPixelFormat.R8G8Sscaled);
+ // Add(Format.R16G16Uscaled, MTLPixelFormat.R16G16Uscaled);
+ // Add(Format.R16G16Sscaled, MTLPixelFormat.R16G16Sscaled);
+ // Add(Format.R32G32Uscaled, MTLPixelFormat.R32G32Uscaled);
+ // Add(Format.R32G32Sscaled, MTLPixelFormat.R32G32Sscaled);
+ // Add(Format.R8G8B8Uscaled, MTLPixelFormat.R8G8B8Uscaled);
+ // Add(Format.R8G8B8Sscaled, MTLPixelFormat.R8G8B8Sscaled);
+ // Add(Format.R16G16B16Uscaled, MTLPixelFormat.R16G16B16Uscaled);
+ // Add(Format.R16G16B16Sscaled, MTLPixelFormat.R16G16B16Sscaled);
+ // Add(Format.R32G32B32Uscaled, MTLPixelFormat.R32G32B32Uscaled);
+ // Add(Format.R32G32B32Sscaled, MTLPixelFormat.R32G32B32Sscaled);
+ // Add(Format.R8G8B8A8Uscaled, MTLPixelFormat.R8G8B8A8Uscaled);
+ // Add(Format.R8G8B8A8Sscaled, MTLPixelFormat.R8G8B8A8Sscaled);
+ // Add(Format.R16G16B16A16Uscaled, MTLPixelFormat.R16G16B16A16Uscaled);
+ // Add(Format.R16G16B16A16Sscaled, MTLPixelFormat.R16G16B16A16Sscaled);
+ // Add(Format.R32G32B32A32Uscaled, MTLPixelFormat.R32G32B32A32Uscaled);
+ // Add(Format.R32G32B32A32Sscaled, MTLPixelFormat.R32G32B32A32Sscaled);
+ // Add(Format.R10G10B10A2Snorm, MTLPixelFormat.A2B10G10R10SNormPack32);
+ // Add(Format.R10G10B10A2Sint, MTLPixelFormat.A2B10G10R10SintPack32);
+ // Add(Format.R10G10B10A2Uscaled, MTLPixelFormat.A2B10G10R10UscaledPack32);
+ // Add(Format.R10G10B10A2Sscaled, MTLPixelFormat.A2B10G10R10SscaledPack32);
+ Add(Format.Astc4x4Unorm, MTLPixelFormat.ASTC4x4LDR);
+ Add(Format.Astc5x4Unorm, MTLPixelFormat.ASTC5x4LDR);
+ Add(Format.Astc5x5Unorm, MTLPixelFormat.ASTC5x5LDR);
+ Add(Format.Astc6x5Unorm, MTLPixelFormat.ASTC6x5LDR);
+ Add(Format.Astc6x6Unorm, MTLPixelFormat.ASTC6x6LDR);
+ Add(Format.Astc8x5Unorm, MTLPixelFormat.ASTC8x5LDR);
+ Add(Format.Astc8x6Unorm, MTLPixelFormat.ASTC8x6LDR);
+ Add(Format.Astc8x8Unorm, MTLPixelFormat.ASTC8x8LDR);
+ Add(Format.Astc10x5Unorm, MTLPixelFormat.ASTC10x5LDR);
+ Add(Format.Astc10x6Unorm, MTLPixelFormat.ASTC10x6LDR);
+ Add(Format.Astc10x8Unorm, MTLPixelFormat.ASTC10x8LDR);
+ Add(Format.Astc10x10Unorm, MTLPixelFormat.ASTC10x10LDR);
+ Add(Format.Astc12x10Unorm, MTLPixelFormat.ASTC12x10LDR);
+ Add(Format.Astc12x12Unorm, MTLPixelFormat.ASTC12x12LDR);
+ Add(Format.Astc4x4Srgb, MTLPixelFormat.ASTC4x4sRGB);
+ Add(Format.Astc5x4Srgb, MTLPixelFormat.ASTC5x4sRGB);
+ Add(Format.Astc5x5Srgb, MTLPixelFormat.ASTC5x5sRGB);
+ Add(Format.Astc6x5Srgb, MTLPixelFormat.ASTC6x5sRGB);
+ Add(Format.Astc6x6Srgb, MTLPixelFormat.ASTC6x6sRGB);
+ Add(Format.Astc8x5Srgb, MTLPixelFormat.ASTC8x5sRGB);
+ Add(Format.Astc8x6Srgb, MTLPixelFormat.ASTC8x6sRGB);
+ Add(Format.Astc8x8Srgb, MTLPixelFormat.ASTC8x8sRGB);
+ Add(Format.Astc10x5Srgb, MTLPixelFormat.ASTC10x5sRGB);
+ Add(Format.Astc10x6Srgb, MTLPixelFormat.ASTC10x6sRGB);
+ Add(Format.Astc10x8Srgb, MTLPixelFormat.ASTC10x8sRGB);
+ Add(Format.Astc10x10Srgb, MTLPixelFormat.ASTC10x10sRGB);
+ Add(Format.Astc12x10Srgb, MTLPixelFormat.ASTC12x10sRGB);
+ Add(Format.Astc12x12Srgb, MTLPixelFormat.ASTC12x12sRGB);
+ Add(Format.B5G6R5Unorm, MTLPixelFormat.B5G6R5Unorm);
+ Add(Format.B5G5R5A1Unorm, MTLPixelFormat.BGR5A1Unorm);
+ Add(Format.A1B5G5R5Unorm, MTLPixelFormat.A1BGR5Unorm);
+ Add(Format.B8G8R8A8Unorm, MTLPixelFormat.BGRA8Unorm);
+ Add(Format.B8G8R8A8Srgb, MTLPixelFormat.BGRA8UnormsRGB);
+ }
+
+ private static void Add(Format format, MTLPixelFormat mtlFormat)
+ {
+ _table[(int)format] = mtlFormat;
+ }
+
+ public static MTLPixelFormat GetFormat(Format format)
+ {
+ var mtlFormat = _table[(int)format];
+
+ if (IsD24S8(format))
+ {
+ if (!MTLDevice.CreateSystemDefaultDevice().Depth24Stencil8PixelFormatSupported)
+ {
+ mtlFormat = MTLPixelFormat.Depth32FloatStencil8;
+ }
+ }
+
+ if (mtlFormat == MTLPixelFormat.Invalid)
+ {
+ Logger.Error?.PrintMsg(LogClass.Gpu, $"Format {format} is not supported by the host.");
+ }
+
+ return mtlFormat;
+ }
+
+ public static bool IsD24S8(Format format)
+ {
+ return format == Format.D24UnormS8Uint || format == Format.S8UintD24Unorm || format == Format.X8UintD24Unorm;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/HardwareInfo.cs b/src/Ryujinx.Graphics.Metal/HardwareInfo.cs
new file mode 100644
index 000000000..4b3b710f8
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/HardwareInfo.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.Metal
+{
+ static partial class HardwareInfoTools
+ {
+
+ private readonly static IntPtr _kCFAllocatorDefault = IntPtr.Zero;
+ private readonly static UInt32 _kCFStringEncodingASCII = 0x0600;
+ private const string IOKit = "/System/Library/Frameworks/IOKit.framework/IOKit";
+ private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
+
+ [LibraryImport(IOKit, StringMarshalling = StringMarshalling.Utf8)]
+ private static partial IntPtr IOServiceMatching(string name);
+
+ [LibraryImport(IOKit)]
+ private static partial IntPtr IOServiceGetMatchingService(IntPtr mainPort, IntPtr matching);
+
+ [LibraryImport(IOKit)]
+ private static partial IntPtr IORegistryEntryCreateCFProperty(IntPtr entry, IntPtr key, IntPtr allocator, UInt32 options);
+
+ [LibraryImport(CoreFoundation, StringMarshalling = StringMarshalling.Utf8)]
+ private static partial IntPtr CFStringCreateWithCString(IntPtr allocator, string cString, UInt32 encoding);
+
+ [LibraryImport(CoreFoundation)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static partial bool CFStringGetCString(IntPtr theString, IntPtr buffer, long bufferSizes, UInt32 encoding);
+
+ [LibraryImport(CoreFoundation)]
+ public static partial IntPtr CFDataGetBytePtr(IntPtr theData);
+
+ static string GetNameFromId(uint id)
+ {
+ return id switch
+ {
+ 0x1002 => "AMD",
+ 0x106B => "Apple",
+ 0x10DE => "NVIDIA",
+ 0x13B5 => "ARM",
+ 0x8086 => "Intel",
+ _ => $"0x{id:X}"
+ };
+ }
+
+ public static string GetVendor()
+ {
+ var serviceDict = IOServiceMatching("IOGPU");
+ var service = IOServiceGetMatchingService(IntPtr.Zero, serviceDict);
+ var cfString = CFStringCreateWithCString(_kCFAllocatorDefault, "vendor-id", _kCFStringEncodingASCII);
+ var cfProperty = IORegistryEntryCreateCFProperty(service, cfString, _kCFAllocatorDefault, 0);
+
+ byte[] buffer = new byte[4];
+ var bufferPtr = CFDataGetBytePtr(cfProperty);
+ Marshal.Copy(bufferPtr, buffer, 0, buffer.Length);
+
+ var vendorId = BitConverter.ToUInt32(buffer);
+
+ return GetNameFromId(vendorId);
+ }
+
+ public static string GetModel()
+ {
+ var serviceDict = IOServiceMatching("IOGPU");
+ var service = IOServiceGetMatchingService(IntPtr.Zero, serviceDict);
+ var cfString = CFStringCreateWithCString(_kCFAllocatorDefault, "model", _kCFStringEncodingASCII);
+ var cfProperty = IORegistryEntryCreateCFProperty(service, cfString, _kCFAllocatorDefault, 0);
+
+ char[] buffer = new char[64];
+ IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length);
+
+ if (CFStringGetCString(cfProperty, bufferPtr, buffer.Length, _kCFStringEncodingASCII))
+ {
+ var model = Marshal.PtrToStringUTF8(bufferPtr);
+ Marshal.FreeHGlobal(bufferPtr);
+ return model;
+ }
+
+ return "";
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/HashTableSlim.cs b/src/Ryujinx.Graphics.Metal/HashTableSlim.cs
new file mode 100644
index 000000000..a27a53d47
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/HashTableSlim.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Graphics.Metal
+{
+ interface IRefEquatable
+ {
+ bool Equals(ref T other);
+ }
+
+ class HashTableSlim where TKey : IRefEquatable
+ {
+ private const int TotalBuckets = 16; // Must be power of 2
+ private const int TotalBucketsMask = TotalBuckets - 1;
+
+ private struct Entry
+ {
+ public int Hash;
+ public TKey Key;
+ public TValue Value;
+ }
+
+ private struct Bucket
+ {
+ public int Length;
+ public Entry[] Entries;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Span AsSpan()
+ {
+ return Entries == null ? Span.Empty : Entries.AsSpan(0, Length);
+ }
+ }
+
+ private readonly Bucket[] _hashTable = new Bucket[TotalBuckets];
+
+ public IEnumerable Keys
+ {
+ get
+ {
+ foreach (Bucket bucket in _hashTable)
+ {
+ for (int i = 0; i < bucket.Length; i++)
+ {
+ yield return bucket.Entries[i].Key;
+ }
+ }
+ }
+ }
+
+ public IEnumerable Values
+ {
+ get
+ {
+ foreach (Bucket bucket in _hashTable)
+ {
+ for (int i = 0; i < bucket.Length; i++)
+ {
+ yield return bucket.Entries[i].Value;
+ }
+ }
+ }
+ }
+
+ public void Add(ref TKey key, TValue value)
+ {
+ var entry = new Entry
+ {
+ Hash = key.GetHashCode(),
+ Key = key,
+ Value = value,
+ };
+
+ int hashCode = key.GetHashCode();
+ int bucketIndex = hashCode & TotalBucketsMask;
+
+ ref var bucket = ref _hashTable[bucketIndex];
+ if (bucket.Entries != null)
+ {
+ int index = bucket.Length;
+
+ if (index >= bucket.Entries.Length)
+ {
+ Array.Resize(ref bucket.Entries, index + 1);
+ }
+
+ bucket.Entries[index] = entry;
+ }
+ else
+ {
+ bucket.Entries = new[]
+ {
+ entry,
+ };
+ }
+
+ bucket.Length++;
+ }
+
+ public bool Remove(ref TKey key)
+ {
+ int hashCode = key.GetHashCode();
+
+ ref var bucket = ref _hashTable[hashCode & TotalBucketsMask];
+ var entries = bucket.AsSpan();
+ for (int i = 0; i < entries.Length; i++)
+ {
+ ref var entry = ref entries[i];
+
+ if (entry.Hash == hashCode && entry.Key.Equals(ref key))
+ {
+ entries[(i + 1)..].CopyTo(entries[i..]);
+ bucket.Length--;
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool TryGetValue(ref TKey key, out TValue value)
+ {
+ int hashCode = key.GetHashCode();
+
+ var entries = _hashTable[hashCode & TotalBucketsMask].AsSpan();
+ for (int i = 0; i < entries.Length; i++)
+ {
+ ref var entry = ref entries[i];
+
+ if (entry.Hash == hashCode && entry.Key.Equals(ref key))
+ {
+ value = entry.Value;
+ return true;
+ }
+ }
+
+ value = default;
+ return false;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/HelperShader.cs b/src/Ryujinx.Graphics.Metal/HelperShader.cs
new file mode 100644
index 000000000..53f503207
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/HelperShader.cs
@@ -0,0 +1,868 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.Translation;
+using SharpMetal.Metal;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class HelperShader : IDisposable
+ {
+ private const int ConvertElementsPerWorkgroup = 32 * 100; // Work group size of 32 times 100 elements.
+ private const string ShadersSourcePath = "/Ryujinx.Graphics.Metal/Shaders";
+ private readonly MetalRenderer _renderer;
+ private readonly Pipeline _pipeline;
+ private MTLDevice _device;
+
+ private readonly ISampler _samplerLinear;
+ private readonly ISampler _samplerNearest;
+ private readonly IProgram _programColorBlitF;
+ private readonly IProgram _programColorBlitI;
+ private readonly IProgram _programColorBlitU;
+ private readonly IProgram _programColorBlitMsF;
+ private readonly IProgram _programColorBlitMsI;
+ private readonly IProgram _programColorBlitMsU;
+ private readonly List _programsColorClearF = new();
+ private readonly List _programsColorClearI = new();
+ private readonly List _programsColorClearU = new();
+ private readonly IProgram _programDepthStencilClear;
+ private readonly IProgram _programStrideChange;
+ private readonly IProgram _programConvertD32S8ToD24S8;
+ private readonly IProgram _programConvertIndexBuffer;
+ private readonly IProgram _programDepthBlit;
+ private readonly IProgram _programDepthBlitMs;
+ private readonly IProgram _programStencilBlit;
+ private readonly IProgram _programStencilBlitMs;
+
+ private readonly EncoderState _helperShaderState = new();
+
+ public HelperShader(MTLDevice device, MetalRenderer renderer, Pipeline pipeline)
+ {
+ _device = device;
+ _renderer = renderer;
+ _pipeline = pipeline;
+
+ _samplerNearest = new SamplerHolder(renderer, _device, SamplerCreateInfo.Create(MinFilter.Nearest, MagFilter.Nearest));
+ _samplerLinear = new SamplerHolder(renderer, _device, SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear));
+
+ var blitResourceLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Vertex, ResourceType.UniformBuffer, 0)
+ .Add(ResourceStages.Fragment, ResourceType.TextureAndSampler, 0).Build();
+
+ var blitSource = ReadMsl("Blit.metal");
+
+ var blitSourceF = blitSource.Replace("FORMAT", "float", StringComparison.Ordinal);
+ _programColorBlitF = new Program(renderer, device, [
+ new ShaderSource(blitSourceF, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var blitSourceI = blitSource.Replace("FORMAT", "int");
+ _programColorBlitI = new Program(renderer, device, [
+ new ShaderSource(blitSourceI, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceI, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var blitSourceU = blitSource.Replace("FORMAT", "uint");
+ _programColorBlitU = new Program(renderer, device, [
+ new ShaderSource(blitSourceU, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceU, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var blitMsSource = ReadMsl("BlitMs.metal");
+
+ var blitMsSourceF = blitMsSource.Replace("FORMAT", "float");
+ _programColorBlitMsF = new Program(renderer, device, [
+ new ShaderSource(blitMsSourceF, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitMsSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var blitMsSourceI = blitMsSource.Replace("FORMAT", "int");
+ _programColorBlitMsI = new Program(renderer, device, [
+ new ShaderSource(blitMsSourceI, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitMsSourceI, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var blitMsSourceU = blitMsSource.Replace("FORMAT", "uint");
+ _programColorBlitMsU = new Program(renderer, device, [
+ new ShaderSource(blitMsSourceU, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitMsSourceU, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var colorClearResourceLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Fragment, ResourceType.UniformBuffer, 0).Build();
+
+ var colorClearSource = ReadMsl("ColorClear.metal");
+
+ for (int i = 0; i < Constants.MaxColorAttachments; i++)
+ {
+ var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "float");
+ _programsColorClearF.Add(new Program(renderer, device, [
+ new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], colorClearResourceLayout));
+ }
+
+ for (int i = 0; i < Constants.MaxColorAttachments; i++)
+ {
+ var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "int");
+ _programsColorClearI.Add(new Program(renderer, device, [
+ new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], colorClearResourceLayout));
+ }
+
+ for (int i = 0; i < Constants.MaxColorAttachments; i++)
+ {
+ var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "uint");
+ _programsColorClearU.Add(new Program(renderer, device, [
+ new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], colorClearResourceLayout));
+ }
+
+ var depthStencilClearSource = ReadMsl("DepthStencilClear.metal");
+ _programDepthStencilClear = new Program(renderer, device, [
+ new ShaderSource(depthStencilClearSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(depthStencilClearSource, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], colorClearResourceLayout);
+
+ var strideChangeResourceLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build();
+
+ var strideChangeSource = ReadMsl("ChangeBufferStride.metal");
+ _programStrideChange = new Program(renderer, device, [
+ new ShaderSource(strideChangeSource, ShaderStage.Compute, TargetLanguage.Msl)
+ ], strideChangeResourceLayout, new ComputeSize(64, 1, 1));
+
+ var convertD32S8ToD24S8ResourceLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build();
+
+ var convertD32S8ToD24S8Source = ReadMsl("ConvertD32S8ToD24S8.metal");
+ _programConvertD32S8ToD24S8 = new Program(renderer, device, [
+ new ShaderSource(convertD32S8ToD24S8Source, ShaderStage.Compute, TargetLanguage.Msl)
+ ], convertD32S8ToD24S8ResourceLayout, new ComputeSize(64, 1, 1));
+
+ var convertIndexBufferLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true)
+ .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 3).Build();
+
+ var convertIndexBufferSource = ReadMsl("ConvertIndexBuffer.metal");
+ _programConvertIndexBuffer = new Program(renderer, device, [
+ new ShaderSource(convertIndexBufferSource, ShaderStage.Compute, TargetLanguage.Msl)
+ ], convertIndexBufferLayout, new ComputeSize(16, 1, 1));
+
+ var depthBlitSource = ReadMsl("DepthBlit.metal");
+ _programDepthBlit = new Program(renderer, device, [
+ new ShaderSource(depthBlitSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var depthBlitMsSource = ReadMsl("DepthBlitMs.metal");
+ _programDepthBlitMs = new Program(renderer, device, [
+ new ShaderSource(depthBlitMsSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var stencilBlitSource = ReadMsl("StencilBlit.metal");
+ _programStencilBlit = new Program(renderer, device, [
+ new ShaderSource(stencilBlitSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+
+ var stencilBlitMsSource = ReadMsl("StencilBlitMs.metal");
+ _programStencilBlitMs = new Program(renderer, device, [
+ new ShaderSource(stencilBlitMsSource, ShaderStage.Fragment, TargetLanguage.Msl),
+ new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl)
+ ], blitResourceLayout);
+ }
+
+ private static string ReadMsl(string fileName)
+ {
+ var msl = EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName));
+
+#pragma warning disable IDE0055 // Disable formatting
+ msl = msl.Replace("CONSTANT_BUFFERS_INDEX", $"{Constants.ConstantBuffersIndex}")
+ .Replace("STORAGE_BUFFERS_INDEX", $"{Constants.StorageBuffersIndex}")
+ .Replace("TEXTURES_INDEX", $"{Constants.TexturesIndex}")
+ .Replace("IMAGES_INDEX", $"{Constants.ImagesIndex}");
+#pragma warning restore IDE0055
+
+ return msl;
+ }
+
+ public unsafe void BlitColor(
+ CommandBufferScoped cbs,
+ Texture src,
+ Texture dst,
+ Extents2D srcRegion,
+ Extents2D dstRegion,
+ bool linearFilter,
+ bool clear = false)
+ {
+ _pipeline.SwapState(_helperShaderState);
+
+ const int RegionBufferSize = 16;
+
+ var sampler = linearFilter ? _samplerLinear : _samplerNearest;
+
+ _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, sampler);
+
+ Span region = stackalloc float[RegionBufferSize / sizeof(float)];
+
+ region[0] = srcRegion.X1 / (float)src.Width;
+ region[1] = srcRegion.X2 / (float)src.Width;
+ region[2] = srcRegion.Y1 / (float)src.Height;
+ region[3] = srcRegion.Y2 / (float)src.Height;
+
+ if (dstRegion.X1 > dstRegion.X2)
+ {
+ (region[0], region[1]) = (region[1], region[0]);
+ }
+
+ if (dstRegion.Y1 > dstRegion.Y2)
+ {
+ (region[2], region[3]) = (region[3], region[2]);
+ }
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, RegionBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, region);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ var rect = new Rectangle(
+ MathF.Min(dstRegion.X1, dstRegion.X2),
+ MathF.Min(dstRegion.Y1, dstRegion.Y2),
+ MathF.Abs(dstRegion.X2 - dstRegion.X1),
+ MathF.Abs(dstRegion.Y2 - dstRegion.Y1));
+
+ Span viewports = stackalloc Viewport[16];
+
+ viewports[0] = new Viewport(
+ rect,
+ ViewportSwizzle.PositiveX,
+ ViewportSwizzle.PositiveY,
+ ViewportSwizzle.PositiveZ,
+ ViewportSwizzle.PositiveW,
+ 0f,
+ 1f);
+
+ bool dstIsDepthOrStencil = dst.Info.Format.IsDepthOrStencil();
+
+ if (dstIsDepthOrStencil)
+ {
+ // TODO: Depth & stencil blit!
+ Logger.Warning?.PrintMsg(LogClass.Gpu, "Requested a depth or stencil blit!");
+ _pipeline.SwapState(null);
+ return;
+ }
+
+ var debugGroupName = "Blit Color ";
+
+ if (src.Info.Target.IsMultisample())
+ {
+ if (dst.Info.Format.IsSint())
+ {
+ debugGroupName += "MS Int";
+ _pipeline.SetProgram(_programColorBlitMsI);
+ }
+ else if (dst.Info.Format.IsUint())
+ {
+ debugGroupName += "MS UInt";
+ _pipeline.SetProgram(_programColorBlitMsU);
+ }
+ else
+ {
+ debugGroupName += "MS Float";
+ _pipeline.SetProgram(_programColorBlitMsF);
+ }
+ }
+ else
+ {
+ if (dst.Info.Format.IsSint())
+ {
+ debugGroupName += "Int";
+ _pipeline.SetProgram(_programColorBlitI);
+ }
+ else if (dst.Info.Format.IsUint())
+ {
+ debugGroupName += "UInt";
+ _pipeline.SetProgram(_programColorBlitU);
+ }
+ else
+ {
+ debugGroupName += "Float";
+ _pipeline.SetProgram(_programColorBlitF);
+ }
+ }
+
+ int dstWidth = dst.Width;
+ int dstHeight = dst.Height;
+
+ Span> scissors = stackalloc Rectangle[16];
+
+ scissors[0] = new Rectangle(0, 0, dstWidth, dstHeight);
+
+ _pipeline.SetRenderTargets([dst], null);
+ _pipeline.SetScissors(scissors);
+
+ _pipeline.SetClearLoadAction(clear);
+
+ _pipeline.SetViewports(viewports);
+ _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+ _pipeline.Draw(4, 1, 0, 0, debugGroupName);
+
+ // Cleanup
+ if (clear)
+ {
+ _pipeline.SetClearLoadAction(false);
+ }
+
+ // Restore previous state
+ _pipeline.SwapState(null);
+ }
+
+ public unsafe void BlitDepthStencil(
+ CommandBufferScoped cbs,
+ Texture src,
+ Texture dst,
+ Extents2D srcRegion,
+ Extents2D dstRegion)
+ {
+ _pipeline.SwapState(_helperShaderState);
+
+ const int RegionBufferSize = 16;
+
+ Span region = stackalloc float[RegionBufferSize / sizeof(float)];
+
+ region[0] = srcRegion.X1 / (float)src.Width;
+ region[1] = srcRegion.X2 / (float)src.Width;
+ region[2] = srcRegion.Y1 / (float)src.Height;
+ region[3] = srcRegion.Y2 / (float)src.Height;
+
+ if (dstRegion.X1 > dstRegion.X2)
+ {
+ (region[0], region[1]) = (region[1], region[0]);
+ }
+
+ if (dstRegion.Y1 > dstRegion.Y2)
+ {
+ (region[2], region[3]) = (region[3], region[2]);
+ }
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, RegionBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, region);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ Span viewports = stackalloc Viewport[16];
+
+ var rect = new Rectangle(
+ MathF.Min(dstRegion.X1, dstRegion.X2),
+ MathF.Min(dstRegion.Y1, dstRegion.Y2),
+ MathF.Abs(dstRegion.X2 - dstRegion.X1),
+ MathF.Abs(dstRegion.Y2 - dstRegion.Y1));
+
+ viewports[0] = new Viewport(
+ rect,
+ ViewportSwizzle.PositiveX,
+ ViewportSwizzle.PositiveY,
+ ViewportSwizzle.PositiveZ,
+ ViewportSwizzle.PositiveW,
+ 0f,
+ 1f);
+
+ int dstWidth = dst.Width;
+ int dstHeight = dst.Height;
+
+ Span> scissors = stackalloc Rectangle[16];
+
+ scissors[0] = new Rectangle(0, 0, dstWidth, dstHeight);
+
+ _pipeline.SetRenderTargets([], dst);
+ _pipeline.SetScissors(scissors);
+ _pipeline.SetViewports(viewports);
+ _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+
+ if (src.Info.Format is
+ Format.D16Unorm or
+ Format.D32Float or
+ Format.X8UintD24Unorm or
+ Format.D24UnormS8Uint or
+ Format.D32FloatS8Uint or
+ Format.S8UintD24Unorm)
+ {
+ var depthTexture = CreateDepthOrStencilView(src, DepthStencilMode.Depth);
+
+ BlitDepthStencilDraw(depthTexture, isDepth: true);
+
+ if (depthTexture != src)
+ {
+ depthTexture.Release();
+ }
+ }
+
+ if (src.Info.Format is
+ Format.S8Uint or
+ Format.D24UnormS8Uint or
+ Format.D32FloatS8Uint or
+ Format.S8UintD24Unorm)
+ {
+ var stencilTexture = CreateDepthOrStencilView(src, DepthStencilMode.Stencil);
+
+ BlitDepthStencilDraw(stencilTexture, isDepth: false);
+
+ if (stencilTexture != src)
+ {
+ stencilTexture.Release();
+ }
+ }
+
+ // Restore previous state
+ _pipeline.SwapState(null);
+ }
+
+ private static Texture CreateDepthOrStencilView(Texture depthStencilTexture, DepthStencilMode depthStencilMode)
+ {
+ if (depthStencilTexture.Info.DepthStencilMode == depthStencilMode)
+ {
+ return depthStencilTexture;
+ }
+
+ return (Texture)depthStencilTexture.CreateView(new TextureCreateInfo(
+ depthStencilTexture.Info.Width,
+ depthStencilTexture.Info.Height,
+ depthStencilTexture.Info.Depth,
+ depthStencilTexture.Info.Levels,
+ depthStencilTexture.Info.Samples,
+ depthStencilTexture.Info.BlockWidth,
+ depthStencilTexture.Info.BlockHeight,
+ depthStencilTexture.Info.BytesPerPixel,
+ depthStencilTexture.Info.Format,
+ depthStencilMode,
+ depthStencilTexture.Info.Target,
+ SwizzleComponent.Red,
+ SwizzleComponent.Green,
+ SwizzleComponent.Blue,
+ SwizzleComponent.Alpha), 0, 0);
+ }
+
+ private void BlitDepthStencilDraw(Texture src, bool isDepth)
+ {
+ // TODO: Check this https://github.com/Ryujinx/Ryujinx/pull/5003/
+ _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, _samplerNearest);
+
+ string debugGroupName;
+
+ if (isDepth)
+ {
+ debugGroupName = "Depth Blit";
+ _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit);
+ _pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always));
+ }
+ else
+ {
+ debugGroupName = "Stencil Blit";
+ _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programStencilBlitMs : _programStencilBlit);
+ _pipeline.SetStencilTest(CreateStencilTestDescriptor(true));
+ }
+
+ _pipeline.Draw(4, 1, 0, 0, debugGroupName);
+
+ if (isDepth)
+ {
+ _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always));
+ }
+ else
+ {
+ _pipeline.SetStencilTest(CreateStencilTestDescriptor(false));
+ }
+ }
+
+ public unsafe void DrawTexture(
+ ITexture src,
+ ISampler srcSampler,
+ Extents2DF srcRegion,
+ Extents2DF dstRegion)
+ {
+ // Save current state
+ var state = _pipeline.SavePredrawState();
+
+ _pipeline.SetFaceCulling(false, Face.Front);
+ _pipeline.SetStencilTest(new StencilTestDescriptor());
+ _pipeline.SetDepthTest(new DepthTestDescriptor());
+
+ const int RegionBufferSize = 16;
+
+ _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, srcSampler);
+
+ Span region = stackalloc float[RegionBufferSize / sizeof(float)];
+
+ region[0] = srcRegion.X1 / src.Width;
+ region[1] = srcRegion.X2 / src.Width;
+ region[2] = srcRegion.Y1 / src.Height;
+ region[3] = srcRegion.Y2 / src.Height;
+
+ if (dstRegion.X1 > dstRegion.X2)
+ {
+ (region[0], region[1]) = (region[1], region[0]);
+ }
+
+ if (dstRegion.Y1 > dstRegion.Y2)
+ {
+ (region[2], region[3]) = (region[3], region[2]);
+ }
+
+ var bufferHandle = _renderer.BufferManager.CreateWithHandle(RegionBufferSize);
+ _renderer.BufferManager.SetData(bufferHandle, 0, region);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, new BufferRange(bufferHandle, 0, RegionBufferSize))]);
+
+ Span viewports = stackalloc Viewport[16];
+
+ var rect = new Rectangle(
+ MathF.Min(dstRegion.X1, dstRegion.X2),
+ MathF.Min(dstRegion.Y1, dstRegion.Y2),
+ MathF.Abs(dstRegion.X2 - dstRegion.X1),
+ MathF.Abs(dstRegion.Y2 - dstRegion.Y1));
+
+ viewports[0] = new Viewport(
+ rect,
+ ViewportSwizzle.PositiveX,
+ ViewportSwizzle.PositiveY,
+ ViewportSwizzle.PositiveZ,
+ ViewportSwizzle.PositiveW,
+ 0f,
+ 1f);
+
+ _pipeline.SetProgram(_programColorBlitF);
+ _pipeline.SetViewports(viewports);
+ _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+ _pipeline.Draw(4, 1, 0, 0, "Draw Texture");
+
+ _renderer.BufferManager.Delete(bufferHandle);
+
+ // Restore previous state
+ _pipeline.RestorePredrawState(state);
+ }
+
+ public void ConvertI8ToI16(CommandBufferScoped cbs, BufferHolder src, BufferHolder dst, int srcOffset, int size)
+ {
+ ChangeStride(cbs, src, dst, srcOffset, size, 1, 2);
+ }
+
+ public unsafe void ChangeStride(
+ CommandBufferScoped cbs,
+ BufferHolder src,
+ BufferHolder dst,
+ int srcOffset,
+ int size,
+ int stride,
+ int newStride)
+ {
+ int elems = size / stride;
+
+ var srcBuffer = src.GetBuffer();
+ var dstBuffer = dst.GetBuffer();
+
+ const int ParamsBufferSize = 4 * sizeof(int);
+
+ // Save current state
+ _pipeline.SwapState(_helperShaderState);
+
+ Span shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)];
+
+ shaderParams[0] = stride;
+ shaderParams[1] = newStride;
+ shaderParams[2] = size;
+ shaderParams[3] = srcOffset;
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ Span> sbRanges = new Auto[2];
+
+ sbRanges[0] = srcBuffer;
+ sbRanges[1] = dstBuffer;
+ _pipeline.SetStorageBuffers(1, sbRanges);
+
+ _pipeline.SetProgram(_programStrideChange);
+ _pipeline.DispatchCompute(1 + elems / ConvertElementsPerWorkgroup, 1, 1, "Change Stride");
+
+ // Restore previous state
+ _pipeline.SwapState(null);
+ }
+
+ public unsafe void ConvertD32S8ToD24S8(CommandBufferScoped cbs, BufferHolder src, Auto dstBuffer, int pixelCount, int dstOffset)
+ {
+ int inSize = pixelCount * 2 * sizeof(int);
+
+ var srcBuffer = src.GetBuffer();
+
+ const int ParamsBufferSize = sizeof(int) * 2;
+
+ // Save current state
+ _pipeline.SwapState(_helperShaderState);
+
+ Span shaderParams = stackalloc int[2];
+
+ shaderParams[0] = pixelCount;
+ shaderParams[1] = dstOffset;
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ Span> sbRanges = new Auto[2];
+
+ sbRanges[0] = srcBuffer;
+ sbRanges[1] = dstBuffer;
+ _pipeline.SetStorageBuffers(1, sbRanges);
+
+ _pipeline.SetProgram(_programConvertD32S8ToD24S8);
+ _pipeline.DispatchCompute(1 + inSize / ConvertElementsPerWorkgroup, 1, 1, "D32S8 to D24S8 Conversion");
+
+ // Restore previous state
+ _pipeline.SwapState(null);
+ }
+
+ public void ConvertIndexBuffer(
+ CommandBufferScoped cbs,
+ BufferHolder src,
+ BufferHolder dst,
+ IndexBufferPattern pattern,
+ int indexSize,
+ int srcOffset,
+ int indexCount)
+ {
+ // TODO: Support conversion with primitive restart enabled.
+
+ int primitiveCount = pattern.GetPrimitiveCount(indexCount);
+ int outputIndexSize = 4;
+
+ var srcBuffer = src.GetBuffer();
+ var dstBuffer = dst.GetBuffer();
+
+ const int ParamsBufferSize = 16 * sizeof(int);
+
+ // Save current state
+ _pipeline.SwapState(_helperShaderState);
+
+ Span shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)];
+
+ shaderParams[8] = pattern.PrimitiveVertices;
+ shaderParams[9] = pattern.PrimitiveVerticesOut;
+ shaderParams[10] = indexSize;
+ shaderParams[11] = outputIndexSize;
+ shaderParams[12] = pattern.BaseIndex;
+ shaderParams[13] = pattern.IndexStride;
+ shaderParams[14] = srcOffset;
+ shaderParams[15] = primitiveCount;
+
+ pattern.OffsetIndex.CopyTo(shaderParams[..pattern.OffsetIndex.Length]);
+
+ using var patternScoped = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize);
+ patternScoped.Holder.SetDataUnchecked(patternScoped.Offset, shaderParams);
+
+ Span> sbRanges = new Auto[2];
+
+ sbRanges[0] = srcBuffer;
+ sbRanges[1] = dstBuffer;
+ _pipeline.SetStorageBuffers(1, sbRanges);
+ _pipeline.SetStorageBuffers([new BufferAssignment(3, patternScoped.Range)]);
+
+ _pipeline.SetProgram(_programConvertIndexBuffer);
+ _pipeline.DispatchCompute(BitUtils.DivRoundUp(primitiveCount, 16), 1, 1, "Convert Index Buffer");
+
+ // Restore previous state
+ _pipeline.SwapState(null);
+ }
+
+ public unsafe void ClearColor(
+ int index,
+ ReadOnlySpan clearColor,
+ uint componentMask,
+ int dstWidth,
+ int dstHeight,
+ Format format)
+ {
+ // Keep original scissor
+ DirtyFlags clearFlags = DirtyFlags.All & (~DirtyFlags.Scissors);
+
+ // Save current state
+ EncoderState originalState = _pipeline.SwapState(_helperShaderState, clearFlags, false);
+
+ // Inherit some state without fully recreating render pipeline.
+ RenderTargetCopy save = _helperShaderState.InheritForClear(originalState, false, index);
+
+ const int ClearColorBufferSize = 16;
+
+ // TODO: Flush
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(_pipeline.Cbs, ClearColorBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, clearColor);
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ Span viewports = stackalloc Viewport[16];
+
+ // TODO: Set exact viewport!
+ viewports[0] = new Viewport(
+ new Rectangle(0, 0, dstWidth, dstHeight),
+ ViewportSwizzle.PositiveX,
+ ViewportSwizzle.PositiveY,
+ ViewportSwizzle.PositiveZ,
+ ViewportSwizzle.PositiveW,
+ 0f,
+ 1f);
+
+ Span componentMasks = stackalloc uint[index + 1];
+ componentMasks[index] = componentMask;
+
+ var debugGroupName = "Clear Color ";
+
+ if (format.IsSint())
+ {
+ debugGroupName += "Int";
+ _pipeline.SetProgram(_programsColorClearI[index]);
+ }
+ else if (format.IsUint())
+ {
+ debugGroupName += "UInt";
+ _pipeline.SetProgram(_programsColorClearU[index]);
+ }
+ else
+ {
+ debugGroupName += "Float";
+ _pipeline.SetProgram(_programsColorClearF[index]);
+ }
+
+ _pipeline.SetBlendState(index, new BlendDescriptor());
+ _pipeline.SetFaceCulling(false, Face.Front);
+ _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always));
+ _pipeline.SetRenderTargetColorMasks(componentMasks);
+ _pipeline.SetViewports(viewports);
+ _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+ _pipeline.Draw(4, 1, 0, 0, debugGroupName);
+
+ // Restore previous state
+ _pipeline.SwapState(null, clearFlags, false);
+
+ _helperShaderState.Restore(save);
+ }
+
+ public unsafe void ClearDepthStencil(
+ float depthValue,
+ bool depthMask,
+ int stencilValue,
+ int stencilMask,
+ int dstWidth,
+ int dstHeight)
+ {
+ // Keep original scissor
+ DirtyFlags clearFlags = DirtyFlags.All & (~DirtyFlags.Scissors);
+ var helperScissors = _helperShaderState.Scissors;
+
+ // Save current state
+ EncoderState originalState = _pipeline.SwapState(_helperShaderState, clearFlags, false);
+
+ // Inherit some state without fully recreating render pipeline.
+ RenderTargetCopy save = _helperShaderState.InheritForClear(originalState, true);
+
+ const int ClearDepthBufferSize = 16;
+
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(_pipeline.Cbs, ClearDepthBufferSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, new ReadOnlySpan(ref depthValue));
+ _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]);
+
+ Span viewports = stackalloc Viewport[1];
+
+ viewports[0] = new Viewport(
+ new Rectangle(0, 0, dstWidth, dstHeight),
+ ViewportSwizzle.PositiveX,
+ ViewportSwizzle.PositiveY,
+ ViewportSwizzle.PositiveZ,
+ ViewportSwizzle.PositiveW,
+ 0f,
+ 1f);
+
+ _pipeline.SetProgram(_programDepthStencilClear);
+ _pipeline.SetFaceCulling(false, Face.Front);
+ _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
+ _pipeline.SetViewports(viewports);
+ _pipeline.SetDepthTest(new DepthTestDescriptor(true, depthMask, CompareOp.Always));
+ _pipeline.SetStencilTest(CreateStencilTestDescriptor(stencilMask != 0, stencilValue, 0xFF, stencilMask));
+ _pipeline.Draw(4, 1, 0, 0, "Clear Depth Stencil");
+
+ // Cleanup
+ _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always));
+ _pipeline.SetStencilTest(CreateStencilTestDescriptor(false));
+
+ // Restore previous state
+ _pipeline.SwapState(null, clearFlags, false);
+
+ _helperShaderState.Restore(save);
+ }
+
+ private static StencilTestDescriptor CreateStencilTestDescriptor(
+ bool enabled,
+ int refValue = 0,
+ int compareMask = 0xff,
+ int writeMask = 0xff)
+ {
+ return new StencilTestDescriptor(
+ enabled,
+ CompareOp.Always,
+ StencilOp.Replace,
+ StencilOp.Replace,
+ StencilOp.Replace,
+ refValue,
+ compareMask,
+ writeMask,
+ CompareOp.Always,
+ StencilOp.Replace,
+ StencilOp.Replace,
+ StencilOp.Replace,
+ refValue,
+ compareMask,
+ writeMask);
+ }
+
+ public void Dispose()
+ {
+ _programColorBlitF.Dispose();
+ _programColorBlitI.Dispose();
+ _programColorBlitU.Dispose();
+ _programColorBlitMsF.Dispose();
+ _programColorBlitMsI.Dispose();
+ _programColorBlitMsU.Dispose();
+
+ foreach (var programColorClear in _programsColorClearF)
+ {
+ programColorClear.Dispose();
+ }
+
+ foreach (var programColorClear in _programsColorClearU)
+ {
+ programColorClear.Dispose();
+ }
+
+ foreach (var programColorClear in _programsColorClearI)
+ {
+ programColorClear.Dispose();
+ }
+
+ _programDepthStencilClear.Dispose();
+ _pipeline.Dispose();
+ _samplerLinear.Dispose();
+ _samplerNearest.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/IdList.cs b/src/Ryujinx.Graphics.Metal/IdList.cs
new file mode 100644
index 000000000..2c15a80ef
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/IdList.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Metal
+{
+ class IdList where T : class
+ {
+ private readonly List _list;
+ private int _freeMin;
+
+ public IdList()
+ {
+ _list = new List();
+ _freeMin = 0;
+ }
+
+ public int Add(T value)
+ {
+ int id;
+ int count = _list.Count;
+ id = _list.IndexOf(null, _freeMin);
+
+ if ((uint)id < (uint)count)
+ {
+ _list[id] = value;
+ }
+ else
+ {
+ id = count;
+ _freeMin = id + 1;
+
+ _list.Add(value);
+ }
+
+ return id + 1;
+ }
+
+ public void Remove(int id)
+ {
+ id--;
+
+ int count = _list.Count;
+
+ if ((uint)id >= (uint)count)
+ {
+ return;
+ }
+
+ if (id + 1 == count)
+ {
+ // Trim unused items.
+ int removeIndex = id;
+
+ while (removeIndex > 0 && _list[removeIndex - 1] == null)
+ {
+ removeIndex--;
+ }
+
+ _list.RemoveRange(removeIndex, count - removeIndex);
+
+ if (_freeMin > removeIndex)
+ {
+ _freeMin = removeIndex;
+ }
+ }
+ else
+ {
+ _list[id] = null;
+
+ if (_freeMin > id)
+ {
+ _freeMin = id;
+ }
+ }
+ }
+
+ public bool TryGetValue(int id, out T value)
+ {
+ id--;
+
+ try
+ {
+ if ((uint)id < (uint)_list.Count)
+ {
+ value = _list[id];
+ return value != null;
+ }
+
+ value = null;
+ return false;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ value = null;
+ return false;
+ }
+ catch (IndexOutOfRangeException)
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ public void Clear()
+ {
+ _list.Clear();
+ _freeMin = 0;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ for (int i = 0; i < _list.Count; i++)
+ {
+ if (_list[i] != null)
+ {
+ yield return _list[i];
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/ImageArray.cs b/src/Ryujinx.Graphics.Metal/ImageArray.cs
new file mode 100644
index 000000000..9fa0df09d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/ImageArray.cs
@@ -0,0 +1,74 @@
+using Ryujinx.Graphics.GAL;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ internal class ImageArray : IImageArray
+ {
+ private readonly TextureRef[] _textureRefs;
+ private readonly TextureBuffer[] _bufferTextureRefs;
+
+ private readonly bool _isBuffer;
+ private readonly Pipeline _pipeline;
+
+ public ImageArray(int size, bool isBuffer, Pipeline pipeline)
+ {
+ if (isBuffer)
+ {
+ _bufferTextureRefs = new TextureBuffer[size];
+ }
+ else
+ {
+ _textureRefs = new TextureRef[size];
+ }
+
+ _isBuffer = isBuffer;
+ _pipeline = pipeline;
+ }
+
+ public void SetImages(int index, ITexture[] images)
+ {
+ for (int i = 0; i < images.Length; i++)
+ {
+ ITexture image = images[i];
+
+ if (image is TextureBuffer textureBuffer)
+ {
+ _bufferTextureRefs[index + i] = textureBuffer;
+ }
+ else if (image is Texture texture)
+ {
+ _textureRefs[index + i].Storage = texture;
+ }
+ else if (!_isBuffer)
+ {
+ _textureRefs[index + i].Storage = null;
+ }
+ else
+ {
+ _bufferTextureRefs[index + i] = null;
+ }
+ }
+
+ SetDirty();
+ }
+
+ public TextureRef[] GetTextureRefs()
+ {
+ return _textureRefs;
+ }
+
+ public TextureBuffer[] GetBufferTextureRefs()
+ {
+ return _bufferTextureRefs;
+ }
+
+ private void SetDirty()
+ {
+ _pipeline.DirtyImages();
+ }
+
+ public void Dispose() { }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs b/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs
new file mode 100644
index 000000000..24e3222fe
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs
@@ -0,0 +1,118 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ internal class IndexBufferPattern : IDisposable
+ {
+ public int PrimitiveVertices { get; }
+ public int PrimitiveVerticesOut { get; }
+ public int BaseIndex { get; }
+ public int[] OffsetIndex { get; }
+ public int IndexStride { get; }
+ public bool RepeatStart { get; }
+
+ private readonly MetalRenderer _renderer;
+ private int _currentSize;
+ private BufferHandle _repeatingBuffer;
+
+ public IndexBufferPattern(MetalRenderer renderer,
+ int primitiveVertices,
+ int primitiveVerticesOut,
+ int baseIndex,
+ int[] offsetIndex,
+ int indexStride,
+ bool repeatStart)
+ {
+ PrimitiveVertices = primitiveVertices;
+ PrimitiveVerticesOut = primitiveVerticesOut;
+ BaseIndex = baseIndex;
+ OffsetIndex = offsetIndex;
+ IndexStride = indexStride;
+ RepeatStart = repeatStart;
+
+ _renderer = renderer;
+ }
+
+ public int GetPrimitiveCount(int vertexCount)
+ {
+ return Math.Max(0, (vertexCount - BaseIndex) / IndexStride);
+ }
+
+ public int GetConvertedCount(int indexCount)
+ {
+ int primitiveCount = GetPrimitiveCount(indexCount);
+ return primitiveCount * OffsetIndex.Length;
+ }
+
+ public BufferHandle GetRepeatingBuffer(int vertexCount, out int indexCount)
+ {
+ int primitiveCount = GetPrimitiveCount(vertexCount);
+ indexCount = primitiveCount * PrimitiveVerticesOut;
+
+ int expectedSize = primitiveCount * OffsetIndex.Length;
+
+ if (expectedSize <= _currentSize && _repeatingBuffer != BufferHandle.Null)
+ {
+ return _repeatingBuffer;
+ }
+
+ // Expand the repeating pattern to the number of requested primitives.
+ BufferHandle newBuffer = _renderer.BufferManager.CreateWithHandle(expectedSize * sizeof(int));
+
+ // Copy the old data to the new one.
+ if (_repeatingBuffer != BufferHandle.Null)
+ {
+ _renderer.Pipeline.CopyBuffer(_repeatingBuffer, newBuffer, 0, 0, _currentSize * sizeof(int));
+ _renderer.BufferManager.Delete(_repeatingBuffer);
+ }
+
+ _repeatingBuffer = newBuffer;
+
+ // Add the additional repeats on top.
+ int newPrimitives = primitiveCount;
+ int oldPrimitives = (_currentSize) / OffsetIndex.Length;
+
+ int[] newData;
+
+ newPrimitives -= oldPrimitives;
+ newData = new int[expectedSize - _currentSize];
+
+ int outOffset = 0;
+ int index = oldPrimitives * IndexStride + BaseIndex;
+
+ for (int i = 0; i < newPrimitives; i++)
+ {
+ if (RepeatStart)
+ {
+ // Used for triangle fan
+ newData[outOffset++] = 0;
+ }
+
+ for (int j = RepeatStart ? 1 : 0; j < OffsetIndex.Length; j++)
+ {
+ newData[outOffset++] = index + OffsetIndex[j];
+ }
+
+ index += IndexStride;
+ }
+
+ _renderer.SetBufferData(newBuffer, _currentSize * sizeof(int), MemoryMarshal.Cast(newData));
+ _currentSize = expectedSize;
+
+ return newBuffer;
+ }
+
+ public void Dispose()
+ {
+ if (_repeatingBuffer != BufferHandle.Null)
+ {
+ _renderer.BufferManager.Delete(_repeatingBuffer);
+ _repeatingBuffer = BufferHandle.Null;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/IndexBufferState.cs b/src/Ryujinx.Graphics.Metal/IndexBufferState.cs
new file mode 100644
index 000000000..411df9685
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/IndexBufferState.cs
@@ -0,0 +1,103 @@
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ readonly internal struct IndexBufferState
+ {
+ public static IndexBufferState Null => new(BufferHandle.Null, 0, 0);
+
+ private readonly int _offset;
+ private readonly int _size;
+ private readonly IndexType _type;
+
+ private readonly BufferHandle _handle;
+
+ public IndexBufferState(BufferHandle handle, int offset, int size, IndexType type = IndexType.UInt)
+ {
+ _handle = handle;
+ _offset = offset;
+ _size = size;
+ _type = type;
+ }
+
+ public (MTLBuffer, int, MTLIndexType) GetIndexBuffer(MetalRenderer renderer, CommandBufferScoped cbs)
+ {
+ Auto autoBuffer;
+ int offset, size;
+ MTLIndexType type;
+
+ if (_type == IndexType.UByte)
+ {
+ // Index type is not supported. Convert to I16.
+ autoBuffer = renderer.BufferManager.GetBufferI8ToI16(cbs, _handle, _offset, _size);
+
+ type = MTLIndexType.UInt16;
+ offset = 0;
+ size = _size * 2;
+ }
+ else
+ {
+ autoBuffer = renderer.BufferManager.GetBuffer(_handle, false, out int bufferSize);
+
+ if (_offset >= bufferSize)
+ {
+ autoBuffer = null;
+ }
+
+ type = _type.Convert();
+ offset = _offset;
+ size = _size;
+ }
+
+ if (autoBuffer != null)
+ {
+ DisposableBuffer buffer = autoBuffer.Get(cbs, offset, size);
+
+ return (buffer.Value, offset, type);
+ }
+
+ return (new MTLBuffer(IntPtr.Zero), 0, MTLIndexType.UInt16);
+ }
+
+ public (MTLBuffer, int, MTLIndexType) GetConvertedIndexBuffer(
+ MetalRenderer renderer,
+ CommandBufferScoped cbs,
+ int firstIndex,
+ int indexCount,
+ int convertedCount,
+ IndexBufferPattern pattern)
+ {
+ // Convert the index buffer using the given pattern.
+ int indexSize = GetIndexSize();
+
+ int firstIndexOffset = firstIndex * indexSize;
+
+ var autoBuffer = renderer.BufferManager.GetBufferTopologyConversion(cbs, _handle, _offset + firstIndexOffset, indexCount * indexSize, pattern, indexSize);
+
+ int size = convertedCount * 4;
+
+ if (autoBuffer != null)
+ {
+ DisposableBuffer buffer = autoBuffer.Get(cbs, 0, size);
+
+ return (buffer.Value, 0, MTLIndexType.UInt32);
+ }
+
+ return (new MTLBuffer(IntPtr.Zero), 0, MTLIndexType.UInt32);
+ }
+
+ private int GetIndexSize()
+ {
+ return _type switch
+ {
+ IndexType.UInt => 4,
+ IndexType.UShort => 2,
+ _ => 1,
+ };
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs
new file mode 100644
index 000000000..7afd30886
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs
@@ -0,0 +1,312 @@
+using Ryujinx.Common.Configuration;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader.Translation;
+using SharpMetal.Metal;
+using SharpMetal.QuartzCore;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ public sealed class MetalRenderer : IRenderer
+ {
+ public const int TotalSets = 4;
+
+ private readonly MTLDevice _device;
+ private readonly MTLCommandQueue _queue;
+ private readonly Func _getMetalLayer;
+
+ private Pipeline _pipeline;
+ private Window _window;
+
+ public uint ProgramCount { get; set; }
+
+#pragma warning disable CS0067 // The event is never used
+ public event EventHandler ScreenCaptured;
+#pragma warning restore CS0067
+
+ public bool PreferThreading => true;
+ public IPipeline Pipeline => _pipeline;
+ public IWindow Window => _window;
+
+ internal MTLCommandQueue BackgroundQueue { get; private set; }
+ internal HelperShader HelperShader { get; private set; }
+ internal BufferManager BufferManager { get; private set; }
+ internal CommandBufferPool CommandBufferPool { get; private set; }
+ internal BackgroundResources BackgroundResources { get; private set; }
+ internal Action InterruptAction { get; private set; }
+ internal SyncManager SyncManager { get; private set; }
+
+ internal HashSet Programs { get; }
+ internal HashSet Samplers { get; }
+
+ public MetalRenderer(Func metalLayer)
+ {
+ _device = MTLDevice.CreateSystemDefaultDevice();
+ Programs = new HashSet();
+ Samplers = new HashSet();
+
+ if (_device.ArgumentBuffersSupport != MTLArgumentBuffersTier.Tier2)
+ {
+ throw new NotSupportedException("Metal backend requires Tier 2 Argument Buffer support.");
+ }
+
+ _queue = _device.NewCommandQueue(CommandBufferPool.MaxCommandBuffers + 1);
+ BackgroundQueue = _device.NewCommandQueue(CommandBufferPool.MaxCommandBuffers);
+
+ _getMetalLayer = metalLayer;
+ }
+
+ public void Initialize(GraphicsDebugLevel logLevel)
+ {
+ var layer = _getMetalLayer();
+ layer.Device = _device;
+ layer.FramebufferOnly = false;
+
+ CommandBufferPool = new CommandBufferPool(_queue);
+ _window = new Window(this, layer);
+ _pipeline = new Pipeline(_device, this);
+ BufferManager = new BufferManager(_device, this, _pipeline);
+
+ _pipeline.InitEncoderStateManager(BufferManager);
+
+ BackgroundResources = new BackgroundResources(this);
+ HelperShader = new HelperShader(_device, this, _pipeline);
+ SyncManager = new SyncManager(this);
+ }
+
+ public void BackgroundContextAction(Action action, bool alwaysBackground = false)
+ {
+ // GetData methods should be thread safe, so we can call this directly.
+ // Texture copy (scaled) may also happen in here, so that should also be thread safe.
+
+ action();
+ }
+
+ public BufferHandle CreateBuffer(int size, BufferAccess access)
+ {
+ return BufferManager.CreateWithHandle(size);
+ }
+
+ public BufferHandle CreateBuffer(IntPtr pointer, int size)
+ {
+ return BufferManager.Create(pointer, size);
+ }
+
+ public BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IImageArray CreateImageArray(int size, bool isBuffer)
+ {
+ return new ImageArray(size, isBuffer, _pipeline);
+ }
+
+ public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
+ {
+ ProgramCount++;
+ return new Program(this, _device, shaders, info.ResourceLayout, info.ComputeLocalSize);
+ }
+
+ public ISampler CreateSampler(SamplerCreateInfo info)
+ {
+ return new SamplerHolder(this, _device, info);
+ }
+
+ public ITexture CreateTexture(TextureCreateInfo info)
+ {
+ if (info.Target == Target.TextureBuffer)
+ {
+ return new TextureBuffer(_device, this, _pipeline, info);
+ }
+
+ return new Texture(_device, this, _pipeline, info);
+ }
+
+ public ITextureArray CreateTextureArray(int size, bool isBuffer)
+ {
+ return new TextureArray(size, isBuffer, _pipeline);
+ }
+
+ public bool PrepareHostMapping(IntPtr address, ulong size)
+ {
+ // TODO: Metal Host Mapping
+ return false;
+ }
+
+ public void CreateSync(ulong id, bool strict)
+ {
+ SyncManager.Create(id, strict);
+ }
+
+ public void DeleteBuffer(BufferHandle buffer)
+ {
+ BufferManager.Delete(buffer);
+ }
+
+ public PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size)
+ {
+ return BufferManager.GetData(buffer, offset, size);
+ }
+
+ public Capabilities GetCapabilities()
+ {
+ // TODO: Finalize these values
+ return new Capabilities(
+ api: TargetApi.Metal,
+ vendorName: HardwareInfoTools.GetVendor(),
+ SystemMemoryType.UnifiedMemory,
+ hasFrontFacingBug: false,
+ hasVectorIndexingBug: false,
+ needsFragmentOutputSpecialization: true,
+ reduceShaderPrecision: true,
+ supportsAstcCompression: true,
+ supportsBc123Compression: true,
+ supportsBc45Compression: true,
+ supportsBc67Compression: true,
+ supportsEtc2Compression: true,
+ supports3DTextureCompression: true,
+ supportsBgraFormat: true,
+ supportsR4G4Format: false,
+ supportsR4G4B4A4Format: true,
+ supportsScaledVertexFormats: false,
+ supportsSnormBufferTextureFormat: true,
+ supportsSparseBuffer: false,
+ supports5BitComponentFormat: true,
+ supportsBlendEquationAdvanced: false,
+ supportsFragmentShaderInterlock: true,
+ supportsFragmentShaderOrderingIntel: false,
+ supportsGeometryShader: false,
+ supportsGeometryShaderPassthrough: false,
+ supportsTransformFeedback: false,
+ supportsImageLoadFormatted: false,
+ supportsLayerVertexTessellation: false,
+ supportsMismatchingViewFormat: true,
+ supportsCubemapView: true,
+ supportsNonConstantTextureOffset: false,
+ supportsQuads: false,
+ supportsSeparateSampler: true,
+ supportsShaderBallot: false,
+ supportsShaderBarrierDivergence: false,
+ supportsShaderFloat64: false,
+ supportsTextureGatherOffsets: false,
+ supportsTextureShadowLod: false,
+ supportsVertexStoreAndAtomics: false,
+ supportsViewportIndexVertexTessellation: false,
+ supportsViewportMask: false,
+ supportsViewportSwizzle: false,
+ supportsIndirectParameters: true,
+ supportsDepthClipControl: false,
+ uniformBufferSetIndex: (int)Constants.ConstantBuffersSetIndex,
+ storageBufferSetIndex: (int)Constants.StorageBuffersSetIndex,
+ textureSetIndex: (int)Constants.TexturesSetIndex,
+ imageSetIndex: (int)Constants.ImagesSetIndex,
+ extraSetBaseIndex: TotalSets,
+ maximumExtraSets: (int)Constants.MaximumExtraSets,
+ maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
+ maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage,
+ maximumTexturesPerStage: Constants.MaxTexturesPerStage,
+ maximumImagesPerStage: Constants.MaxImagesPerStage,
+ maximumComputeSharedMemorySize: (int)_device.MaxThreadgroupMemoryLength,
+ maximumSupportedAnisotropy: 16,
+ shaderSubgroupSize: 256,
+ storageBufferOffsetAlignment: 16,
+ textureBufferOffsetAlignment: 16,
+ gatherBiasPrecision: 0,
+ maximumGpuMemory: 0
+ );
+ }
+
+ public ulong GetCurrentSync()
+ {
+ return SyncManager.GetCurrent();
+ }
+
+ public HardwareInfo GetHardwareInfo()
+ {
+ return new HardwareInfo(HardwareInfoTools.GetVendor(), HardwareInfoTools.GetModel(), "Apple");
+ }
+
+ public IProgram LoadProgramBinary(byte[] programBinary, bool hasFragmentShader, ShaderInfo info)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data)
+ {
+ BufferManager.SetData(buffer, offset, data, _pipeline.Cbs);
+ }
+
+ public void UpdateCounters()
+ {
+ // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc
+ }
+
+ public void PreFrame()
+ {
+ SyncManager.Cleanup();
+ }
+
+ public ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler, float divisor, bool hostReserved)
+ {
+ // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc
+ var counterEvent = new CounterEvent();
+ resultHandler?.Invoke(counterEvent, type == CounterType.SamplesPassed ? (ulong)1 : 0);
+ return counterEvent;
+ }
+
+ public void ResetCounter(CounterType type)
+ {
+ // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc
+ }
+
+ public void WaitSync(ulong id)
+ {
+ SyncManager.Wait(id);
+ }
+
+ public void FlushAllCommands()
+ {
+ _pipeline.FlushCommandsImpl();
+ }
+
+ public void RegisterFlush()
+ {
+ SyncManager.RegisterFlush();
+
+ // Periodically free unused regions of the staging buffer to avoid doing it all at once.
+ BufferManager.StagingBuffer.FreeCompleted();
+ }
+
+ public void SetInterruptAction(Action interruptAction)
+ {
+ InterruptAction = interruptAction;
+ }
+
+ public void Screenshot()
+ {
+ // TODO: Screenshots
+ }
+
+ public void Dispose()
+ {
+ BackgroundResources.Dispose();
+
+ foreach (var program in Programs)
+ {
+ program.Dispose();
+ }
+
+ foreach (var sampler in Samplers)
+ {
+ sampler.Dispose();
+ }
+
+ _pipeline.Dispose();
+ _window.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs
new file mode 100644
index 000000000..cd5ad08ba
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs
@@ -0,0 +1,262 @@
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ ///
+ /// Holder for multiple host GPU fences.
+ ///
+ [SupportedOSPlatform("macos")]
+ class MultiFenceHolder
+ {
+ private const int BufferUsageTrackingGranularity = 4096;
+
+ private readonly FenceHolder[] _fences;
+ private readonly BufferUsageBitmap _bufferUsageBitmap;
+
+ ///
+ /// Creates a new instance of the multiple fence holder.
+ ///
+ public MultiFenceHolder()
+ {
+ _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers];
+ }
+
+ ///
+ /// Creates a new instance of the multiple fence holder, with a given buffer size in mind.
+ ///
+ /// Size of the buffer
+ public MultiFenceHolder(int size)
+ {
+ _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers];
+ _bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity);
+ }
+
+ ///
+ /// Adds read/write buffer usage information to the uses list.
+ ///
+ /// Index of the command buffer where the buffer is used
+ /// Offset of the buffer being used
+ /// Size of the buffer region being used, in bytes
+ /// Whether the access is a write or not
+ public void AddBufferUse(int cbIndex, int offset, int size, bool write)
+ {
+ _bufferUsageBitmap.Add(cbIndex, offset, size, false);
+
+ if (write)
+ {
+ _bufferUsageBitmap.Add(cbIndex, offset, size, true);
+ }
+ }
+
+ ///
+ /// Removes all buffer usage information for a given command buffer.
+ ///
+ /// Index of the command buffer where the buffer is used
+ public void RemoveBufferUses(int cbIndex)
+ {
+ _bufferUsageBitmap?.Clear(cbIndex);
+ }
+
+ ///
+ /// Checks if a given range of a buffer is being used by a command buffer still being processed by the GPU.
+ ///
+ /// Index of the command buffer where the buffer is used
+ /// Offset of the buffer being used
+ /// Size of the buffer region being used, in bytes
+ /// True if in use, false otherwise
+ public bool IsBufferRangeInUse(int cbIndex, int offset, int size)
+ {
+ return _bufferUsageBitmap.OverlapsWith(cbIndex, offset, size);
+ }
+
+ ///
+ /// Checks if a given range of a buffer is being used by any command buffer still being processed by the GPU.
+ ///
+ /// Offset of the buffer being used
+ /// Size of the buffer region being used, in bytes
+ /// True if only write usages should count
+ /// True if in use, false otherwise
+ public bool IsBufferRangeInUse(int offset, int size, bool write)
+ {
+ return _bufferUsageBitmap.OverlapsWith(offset, size, write);
+ }
+
+ ///
+ /// Adds a fence to the holder.
+ ///
+ /// Command buffer index of the command buffer that owns the fence
+ /// Fence to be added
+ /// True if the command buffer's previous fence value was null
+ public bool AddFence(int cbIndex, FenceHolder fence)
+ {
+ ref FenceHolder fenceRef = ref _fences[cbIndex];
+
+ if (fenceRef == null)
+ {
+ fenceRef = fence;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Removes a fence from the holder.
+ ///
+ /// Command buffer index of the command buffer that owns the fence
+ public void RemoveFence(int cbIndex)
+ {
+ _fences[cbIndex] = null;
+ }
+
+ ///
+ /// Determines if a fence referenced on the given command buffer.
+ ///
+ /// Index of the command buffer to check if it's used
+ /// True if referenced, false otherwise
+ public bool HasFence(int cbIndex)
+ {
+ return _fences[cbIndex] != null;
+ }
+
+ ///
+ /// Wait until all the fences on the holder are signaled.
+ ///
+ public void WaitForFences()
+ {
+ WaitForFencesImpl(0, 0, true);
+ }
+
+ ///
+ /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled.
+ ///
+ /// Start offset of the buffer range
+ /// Size of the buffer range in bytes
+ public void WaitForFences(int offset, int size)
+ {
+ WaitForFencesImpl(offset, size, true);
+ }
+
+ ///
+ /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled.
+ ///
+
+ // TODO: Add a proper timeout!
+ public bool WaitForFences(bool indefinite)
+ {
+ return WaitForFencesImpl(0, 0, indefinite);
+ }
+
+ ///
+ /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled.
+ ///
+ /// Start offset of the buffer range
+ /// Size of the buffer range in bytes
+ /// Indicates if this should wait indefinitely
+ /// True if all fences were signaled before the timeout expired, false otherwise
+ private bool WaitForFencesImpl(int offset, int size, bool indefinite)
+ {
+ Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers];
+
+ int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders);
+ Span fences = stackalloc MTLCommandBuffer[count];
+
+ int fenceCount = 0;
+
+ for (int i = 0; i < count; i++)
+ {
+ if (fenceHolders[i].TryGet(out MTLCommandBuffer fence))
+ {
+ fences[fenceCount] = fence;
+
+ if (fenceCount < i)
+ {
+ fenceHolders[fenceCount] = fenceHolders[i];
+ }
+
+ fenceCount++;
+ }
+ }
+
+ if (fenceCount == 0)
+ {
+ return true;
+ }
+
+ bool signaled = true;
+
+ if (indefinite)
+ {
+ foreach (var fence in fences)
+ {
+ fence.WaitUntilCompleted();
+ }
+ }
+ else
+ {
+ foreach (var fence in fences)
+ {
+ if (fence.Status != MTLCommandBufferStatus.Completed)
+ {
+ signaled = false;
+ }
+ }
+ }
+
+ for (int i = 0; i < fenceCount; i++)
+ {
+ fenceHolders[i].Put();
+ }
+
+ return signaled;
+ }
+
+ ///
+ /// Gets fences to wait for.
+ ///
+ /// Span to store fences in
+ /// Number of fences placed in storage
+ private int GetFences(Span storage)
+ {
+ int count = 0;
+
+ for (int i = 0; i < _fences.Length; i++)
+ {
+ var fence = _fences[i];
+
+ if (fence != null)
+ {
+ storage[count++] = fence;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// Gets fences to wait for use of a given buffer region.
+ ///
+ /// Span to store overlapping fences in
+ /// Offset of the range
+ /// Size of the range in bytes
+ /// Number of fences for the specified region placed in storage
+ private int GetOverlappingFences(Span storage, int offset, int size)
+ {
+ int count = 0;
+
+ for (int i = 0; i < _fences.Length; i++)
+ {
+ var fence = _fences[i];
+
+ if (fence != null && _bufferUsageBitmap.OverlapsWith(i, offset, size))
+ {
+ storage[count++] = fence;
+ }
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs b/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs
new file mode 100644
index 000000000..fa3df47db
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs
@@ -0,0 +1,99 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ internal class PersistentFlushBuffer : IDisposable
+ {
+ private readonly MetalRenderer _renderer;
+
+ private BufferHolder _flushStorage;
+
+ public PersistentFlushBuffer(MetalRenderer renderer)
+ {
+ _renderer = renderer;
+ }
+
+ private BufferHolder ResizeIfNeeded(int size)
+ {
+ var flushStorage = _flushStorage;
+
+ if (flushStorage == null || size > _flushStorage.Size)
+ {
+ flushStorage?.Dispose();
+
+ flushStorage = _renderer.BufferManager.Create(size);
+ _flushStorage = flushStorage;
+ }
+
+ return flushStorage;
+ }
+
+ public Span GetBufferData(CommandBufferPool cbp, BufferHolder buffer, int offset, int size)
+ {
+ var flushStorage = ResizeIfNeeded(size);
+ Auto srcBuffer;
+
+ using (var cbs = cbp.Rent())
+ {
+ srcBuffer = buffer.GetBuffer();
+ var dstBuffer = flushStorage.GetBuffer();
+
+ if (srcBuffer.TryIncrementReferenceCount())
+ {
+ BufferHolder.Copy(cbs, srcBuffer, dstBuffer, offset, 0, size, registerSrcUsage: false);
+ }
+ else
+ {
+ // Source buffer is no longer alive, don't copy anything to flush storage.
+ srcBuffer = null;
+ }
+ }
+
+ flushStorage.WaitForFences();
+ srcBuffer?.DecrementReferenceCount();
+ return flushStorage.GetDataStorage(0, size);
+ }
+
+ public Span GetTextureData(CommandBufferPool cbp, Texture view, int size)
+ {
+ TextureCreateInfo info = view.Info;
+
+ var flushStorage = ResizeIfNeeded(size);
+
+ using (var cbs = cbp.Rent())
+ {
+ var buffer = flushStorage.GetBuffer().Get(cbs).Value;
+ var image = view.GetHandle();
+
+ view.CopyFromOrToBuffer(cbs, buffer, image, size, true, 0, 0, info.GetLayers(), info.Levels, singleSlice: false);
+ }
+
+ flushStorage.WaitForFences();
+ return flushStorage.GetDataStorage(0, size);
+ }
+
+ public Span GetTextureData(CommandBufferPool cbp, Texture view, int size, int layer, int level)
+ {
+ var flushStorage = ResizeIfNeeded(size);
+
+ using (var cbs = cbp.Rent())
+ {
+ var buffer = flushStorage.GetBuffer().Get(cbs).Value;
+ var image = view.GetHandle();
+
+ view.CopyFromOrToBuffer(cbs, buffer, image, size, true, layer, level, 1, 1, singleSlice: true);
+ }
+
+ flushStorage.WaitForFences();
+ return flushStorage.GetDataStorage(0, size);
+ }
+
+ public void Dispose()
+ {
+ _flushStorage.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Pipeline.cs b/src/Ryujinx.Graphics.Metal/Pipeline.cs
new file mode 100644
index 000000000..113974061
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Pipeline.cs
@@ -0,0 +1,877 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using SharpMetal.Foundation;
+using SharpMetal.Metal;
+using SharpMetal.QuartzCore;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ public enum EncoderType
+ {
+ Blit,
+ Compute,
+ Render,
+ None
+ }
+
+ [SupportedOSPlatform("macos")]
+ class Pipeline : IPipeline, IEncoderFactory, IDisposable
+ {
+ private const ulong MinByteWeightForFlush = 256 * 1024 * 1024; // MiB
+
+ private readonly MTLDevice _device;
+ private readonly MetalRenderer _renderer;
+ private EncoderStateManager _encoderStateManager;
+ private ulong _byteWeight;
+
+ public MTLCommandBuffer CommandBuffer;
+
+ public IndexBufferPattern QuadsToTrisPattern;
+ public IndexBufferPattern TriFanToTrisPattern;
+
+ internal CommandBufferScoped? PreloadCbs { get; private set; }
+ internal CommandBufferScoped Cbs { get; private set; }
+ internal CommandBufferEncoder Encoders => Cbs.Encoders;
+ internal EncoderType CurrentEncoderType => Encoders.CurrentEncoderType;
+
+ public Pipeline(MTLDevice device, MetalRenderer renderer)
+ {
+ _device = device;
+ _renderer = renderer;
+
+ renderer.CommandBufferPool.Initialize(this);
+
+ CommandBuffer = (Cbs = _renderer.CommandBufferPool.Rent()).CommandBuffer;
+ }
+
+ internal void InitEncoderStateManager(BufferManager bufferManager)
+ {
+ _encoderStateManager = new EncoderStateManager(_device, bufferManager, this);
+
+ QuadsToTrisPattern = new IndexBufferPattern(_renderer, 4, 6, 0, [0, 1, 2, 0, 2, 3], 4, false);
+ TriFanToTrisPattern = new IndexBufferPattern(_renderer, 3, 3, 2, [int.MinValue, -1, 0], 1, true);
+ }
+
+ public EncoderState SwapState(EncoderState state, DirtyFlags flags = DirtyFlags.All, bool endRenderPass = true)
+ {
+ if (endRenderPass && CurrentEncoderType == EncoderType.Render)
+ {
+ EndCurrentPass();
+ }
+
+ return _encoderStateManager.SwapState(state, flags);
+ }
+
+ public PredrawState SavePredrawState()
+ {
+ return _encoderStateManager.SavePredrawState();
+ }
+
+ public void RestorePredrawState(PredrawState state)
+ {
+ _encoderStateManager.RestorePredrawState(state);
+ }
+
+ public void SetClearLoadAction(bool clear)
+ {
+ _encoderStateManager.SetClearLoadAction(clear);
+ }
+
+ public MTLRenderCommandEncoder GetOrCreateRenderEncoder(bool forDraw = false)
+ {
+ // Mark all state as dirty to ensure it is set on the new encoder
+ if (Cbs.Encoders.CurrentEncoderType != EncoderType.Render)
+ {
+ _encoderStateManager.SignalRenderDirty();
+ }
+
+ if (forDraw)
+ {
+ _encoderStateManager.RenderResourcesPrepass();
+ }
+
+ MTLRenderCommandEncoder renderCommandEncoder = Cbs.Encoders.EnsureRenderEncoder();
+
+ if (forDraw)
+ {
+ _encoderStateManager.RebindRenderState(renderCommandEncoder);
+ }
+
+ return renderCommandEncoder;
+ }
+
+ public MTLBlitCommandEncoder GetOrCreateBlitEncoder()
+ {
+ return Cbs.Encoders.EnsureBlitEncoder();
+ }
+
+ public MTLComputeCommandEncoder GetOrCreateComputeEncoder(bool forDispatch = false)
+ {
+ // Mark all state as dirty to ensure it is set on the new encoder
+ if (Cbs.Encoders.CurrentEncoderType != EncoderType.Compute)
+ {
+ _encoderStateManager.SignalComputeDirty();
+ }
+
+ if (forDispatch)
+ {
+ _encoderStateManager.ComputeResourcesPrepass();
+ }
+
+ MTLComputeCommandEncoder computeCommandEncoder = Cbs.Encoders.EnsureComputeEncoder();
+
+ if (forDispatch)
+ {
+ _encoderStateManager.RebindComputeState(computeCommandEncoder);
+ }
+
+ return computeCommandEncoder;
+ }
+
+ public void EndCurrentPass()
+ {
+ Cbs.Encoders.EndCurrentPass();
+ }
+
+ public MTLRenderCommandEncoder CreateRenderCommandEncoder()
+ {
+ return _encoderStateManager.CreateRenderCommandEncoder();
+ }
+
+ public MTLComputeCommandEncoder CreateComputeCommandEncoder()
+ {
+ return _encoderStateManager.CreateComputeCommandEncoder();
+ }
+
+ public void Present(CAMetalDrawable drawable, Texture src, Extents2D srcRegion, Extents2D dstRegion, bool isLinear)
+ {
+ // TODO: Clean this up
+ var textureInfo = new TextureCreateInfo((int)drawable.Texture.Width, (int)drawable.Texture.Height, (int)drawable.Texture.Depth, (int)drawable.Texture.MipmapLevelCount, (int)drawable.Texture.SampleCount, 0, 0, 0, Format.B8G8R8A8Unorm, 0, Target.Texture2D, SwizzleComponent.Red, SwizzleComponent.Green, SwizzleComponent.Blue, SwizzleComponent.Alpha);
+ var dst = new Texture(_device, _renderer, this, textureInfo, drawable.Texture, 0, 0);
+
+ _renderer.HelperShader.BlitColor(Cbs, src, dst, srcRegion, dstRegion, isLinear, true);
+
+ EndCurrentPass();
+
+ Cbs.CommandBuffer.PresentDrawable(drawable);
+
+ FlushCommandsImpl();
+
+ // TODO: Auto flush counting
+ _renderer.SyncManager.GetAndResetWaitTicks();
+
+ // Cleanup
+ dst.Dispose();
+ }
+
+ public CommandBufferScoped GetPreloadCommandBuffer()
+ {
+ PreloadCbs ??= _renderer.CommandBufferPool.Rent();
+
+ return PreloadCbs.Value;
+ }
+
+ public void FlushCommandsIfWeightExceeding(IAuto disposedResource, ulong byteWeight)
+ {
+ bool usedByCurrentCb = disposedResource.HasCommandBufferDependency(Cbs);
+
+ if (PreloadCbs != null && !usedByCurrentCb)
+ {
+ usedByCurrentCb = disposedResource.HasCommandBufferDependency(PreloadCbs.Value);
+ }
+
+ if (usedByCurrentCb)
+ {
+ // Since we can only free memory after the command buffer that uses a given resource was executed,
+ // keeping the command buffer might cause a high amount of memory to be in use.
+ // To prevent that, we force submit command buffers if the memory usage by resources
+ // in use by the current command buffer is above a given limit, and those resources were disposed.
+ _byteWeight += byteWeight;
+
+ if (_byteWeight >= MinByteWeightForFlush)
+ {
+ FlushCommandsImpl();
+ }
+ }
+ }
+
+ public void FlushCommandsImpl()
+ {
+ EndCurrentPass();
+
+ _byteWeight = 0;
+
+ if (PreloadCbs != null)
+ {
+ PreloadCbs.Value.Dispose();
+ PreloadCbs = null;
+ }
+
+ CommandBuffer = (Cbs = _renderer.CommandBufferPool.ReturnAndRent(Cbs)).CommandBuffer;
+ _renderer.RegisterFlush();
+ }
+
+ public void DirtyTextures()
+ {
+ _encoderStateManager.DirtyTextures();
+ }
+
+ public void DirtyImages()
+ {
+ _encoderStateManager.DirtyImages();
+ }
+
+ public void Blit(
+ Texture src,
+ Texture dst,
+ Extents2D srcRegion,
+ Extents2D dstRegion,
+ bool isDepthOrStencil,
+ bool linearFilter)
+ {
+ if (isDepthOrStencil)
+ {
+ _renderer.HelperShader.BlitDepthStencil(Cbs, src, dst, srcRegion, dstRegion);
+ }
+ else
+ {
+ _renderer.HelperShader.BlitColor(Cbs, src, dst, srcRegion, dstRegion, linearFilter);
+ }
+ }
+
+ public void Barrier()
+ {
+ switch (CurrentEncoderType)
+ {
+ case EncoderType.Render:
+ {
+ var scope = MTLBarrierScope.Buffers | MTLBarrierScope.Textures | MTLBarrierScope.RenderTargets;
+ MTLRenderStages stages = MTLRenderStages.RenderStageVertex | MTLRenderStages.RenderStageFragment;
+ Encoders.RenderEncoder.MemoryBarrier(scope, stages, stages);
+ break;
+ }
+ case EncoderType.Compute:
+ {
+ var scope = MTLBarrierScope.Buffers | MTLBarrierScope.Textures | MTLBarrierScope.RenderTargets;
+ Encoders.ComputeEncoder.MemoryBarrier(scope);
+ break;
+ }
+ }
+ }
+
+ public void ClearBuffer(BufferHandle destination, int offset, int size, uint value)
+ {
+ var blitCommandEncoder = GetOrCreateBlitEncoder();
+
+ var mtlBuffer = _renderer.BufferManager.GetBuffer(destination, offset, size, true).Get(Cbs, offset, size, true).Value;
+
+ // Might need a closer look, range's count, lower, and upper bound
+ // must be a multiple of 4
+ blitCommandEncoder.FillBuffer(mtlBuffer,
+ new NSRange
+ {
+ location = (ulong)offset,
+ length = (ulong)size
+ },
+ (byte)value);
+ }
+
+ public void ClearRenderTargetColor(int index, int layer, int layerCount, uint componentMask, ColorF color)
+ {
+ float[] colors = [color.Red, color.Green, color.Blue, color.Alpha];
+ var dst = _encoderStateManager.RenderTargets[index];
+
+ // TODO: Remove workaround for Wonder which has an invalid texture due to unsupported format
+ if (dst == null)
+ {
+ Logger.Warning?.PrintMsg(LogClass.Gpu, "Attempted to clear invalid render target!");
+ return;
+ }
+
+ _renderer.HelperShader.ClearColor(index, colors, componentMask, dst.Width, dst.Height, dst.Info.Format);
+ }
+
+ public void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, int stencilMask)
+ {
+ var depthStencil = _encoderStateManager.DepthStencil;
+
+ if (depthStencil == null)
+ {
+ return;
+ }
+
+ _renderer.HelperShader.ClearDepthStencil(depthValue, depthMask, stencilValue, stencilMask, depthStencil.Width, depthStencil.Height);
+ }
+
+ public void CommandBufferBarrier()
+ {
+ Barrier();
+ }
+
+ public void CopyBuffer(BufferHandle src, BufferHandle dst, int srcOffset, int dstOffset, int size)
+ {
+ var srcBuffer = _renderer.BufferManager.GetBuffer(src, srcOffset, size, false);
+ var dstBuffer = _renderer.BufferManager.GetBuffer(dst, dstOffset, size, true);
+
+ BufferHolder.Copy(Cbs, srcBuffer, dstBuffer, srcOffset, dstOffset, size);
+ }
+
+ public void PushDebugGroup(string name)
+ {
+ var encoder = Encoders.CurrentEncoder;
+ var debugGroupName = StringHelper.NSString(name);
+
+ if (encoder == null)
+ {
+ return;
+ }
+
+ switch (Encoders.CurrentEncoderType)
+ {
+ case EncoderType.Render:
+ encoder.Value.PushDebugGroup(debugGroupName);
+ break;
+ case EncoderType.Blit:
+ encoder.Value.PushDebugGroup(debugGroupName);
+ break;
+ case EncoderType.Compute:
+ encoder.Value.PushDebugGroup(debugGroupName);
+ break;
+ }
+ }
+
+ public void PopDebugGroup()
+ {
+ var encoder = Encoders.CurrentEncoder;
+
+ if (encoder == null)
+ {
+ return;
+ }
+
+ switch (Encoders.CurrentEncoderType)
+ {
+ case EncoderType.Render:
+ encoder.Value.PopDebugGroup();
+ break;
+ case EncoderType.Blit:
+ encoder.Value.PopDebugGroup();
+ break;
+ case EncoderType.Compute:
+ encoder.Value.PopDebugGroup();
+ break;
+ }
+ }
+
+ public void DispatchCompute(int groupsX, int groupsY, int groupsZ)
+ {
+ DispatchCompute(groupsX, groupsY, groupsZ, String.Empty);
+ }
+
+ public void DispatchCompute(int groupsX, int groupsY, int groupsZ, string debugGroupName)
+ {
+ var computeCommandEncoder = GetOrCreateComputeEncoder(true);
+
+ ComputeSize localSize = _encoderStateManager.ComputeLocalSize;
+
+ if (debugGroupName != String.Empty)
+ {
+ PushDebugGroup(debugGroupName);
+ }
+
+ computeCommandEncoder.DispatchThreadgroups(
+ new MTLSize { width = (ulong)groupsX, height = (ulong)groupsY, depth = (ulong)groupsZ },
+ new MTLSize { width = (ulong)localSize.X, height = (ulong)localSize.Y, depth = (ulong)localSize.Z });
+
+ if (debugGroupName != String.Empty)
+ {
+ PopDebugGroup();
+ }
+ }
+
+ public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
+ {
+ Draw(vertexCount, instanceCount, firstVertex, firstInstance, String.Empty);
+ }
+
+ public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance, string debugGroupName)
+ {
+ if (vertexCount == 0)
+ {
+ return;
+ }
+
+ var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert();
+
+ if (TopologyUnsupported(_encoderStateManager.Topology))
+ {
+ var pattern = GetIndexBufferPattern();
+
+ BufferHandle handle = pattern.GetRepeatingBuffer(vertexCount, out int indexCount);
+ var buffer = _renderer.BufferManager.GetBuffer(handle, false);
+ var mtlBuffer = buffer.Get(Cbs, 0, indexCount * sizeof(int)).Value;
+
+ var renderCommandEncoder = GetOrCreateRenderEncoder(true);
+
+ renderCommandEncoder.DrawIndexedPrimitives(
+ primitiveType,
+ (ulong)indexCount,
+ MTLIndexType.UInt32,
+ mtlBuffer,
+ 0);
+ }
+ else
+ {
+ var renderCommandEncoder = GetOrCreateRenderEncoder(true);
+
+ if (debugGroupName != String.Empty)
+ {
+ PushDebugGroup(debugGroupName);
+ }
+
+ renderCommandEncoder.DrawPrimitives(
+ primitiveType,
+ (ulong)firstVertex,
+ (ulong)vertexCount,
+ (ulong)instanceCount,
+ (ulong)firstInstance);
+
+ if (debugGroupName != String.Empty)
+ {
+ PopDebugGroup();
+ }
+ }
+ }
+
+ private IndexBufferPattern GetIndexBufferPattern()
+ {
+ return _encoderStateManager.Topology switch
+ {
+ PrimitiveTopology.Quads => QuadsToTrisPattern,
+ PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => TriFanToTrisPattern,
+ _ => throw new NotSupportedException($"Unsupported topology: {_encoderStateManager.Topology}"),
+ };
+ }
+
+ private PrimitiveTopology TopologyRemap(PrimitiveTopology topology)
+ {
+ return topology switch
+ {
+ PrimitiveTopology.Quads => PrimitiveTopology.Triangles,
+ PrimitiveTopology.QuadStrip => PrimitiveTopology.TriangleStrip,
+ PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => PrimitiveTopology.Triangles,
+ _ => topology,
+ };
+ }
+
+ private bool TopologyUnsupported(PrimitiveTopology topology)
+ {
+ return topology switch
+ {
+ PrimitiveTopology.Quads or PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => true,
+ _ => false,
+ };
+ }
+
+ public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
+ {
+ if (indexCount == 0)
+ {
+ return;
+ }
+
+ MTLBuffer mtlBuffer;
+ int offset;
+ MTLIndexType type;
+ int finalIndexCount = indexCount;
+
+ var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert();
+
+ if (TopologyUnsupported(_encoderStateManager.Topology))
+ {
+ var pattern = GetIndexBufferPattern();
+ int convertedCount = pattern.GetConvertedCount(indexCount);
+
+ finalIndexCount = convertedCount;
+
+ (mtlBuffer, offset, type) = _encoderStateManager.IndexBuffer.GetConvertedIndexBuffer(_renderer, Cbs, firstIndex, indexCount, convertedCount, pattern);
+ }
+ else
+ {
+ (mtlBuffer, offset, type) = _encoderStateManager.IndexBuffer.GetIndexBuffer(_renderer, Cbs);
+ }
+
+ if (mtlBuffer.NativePtr != IntPtr.Zero)
+ {
+ var renderCommandEncoder = GetOrCreateRenderEncoder(true);
+
+ renderCommandEncoder.DrawIndexedPrimitives(
+ primitiveType,
+ (ulong)finalIndexCount,
+ type,
+ mtlBuffer,
+ (ulong)offset,
+ (ulong)instanceCount,
+ firstVertex,
+ (ulong)firstInstance);
+ }
+ }
+
+ public void DrawIndexedIndirect(BufferRange indirectBuffer)
+ {
+ DrawIndexedIndirectOffset(indirectBuffer);
+ }
+
+ public void DrawIndexedIndirectOffset(BufferRange indirectBuffer, int offset = 0)
+ {
+ // TODO: Reindex unsupported topologies
+ if (TopologyUnsupported(_encoderStateManager.Topology))
+ {
+ Logger.Warning?.Print(LogClass.Gpu, $"Drawing indexed with unsupported topology: {_encoderStateManager.Topology}");
+ }
+
+ var buffer = _renderer.BufferManager
+ .GetBuffer(indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false)
+ .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
+
+ var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert();
+
+ (MTLBuffer indexBuffer, int indexOffset, MTLIndexType type) = _encoderStateManager.IndexBuffer.GetIndexBuffer(_renderer, Cbs);
+
+ if (indexBuffer.NativePtr != IntPtr.Zero && buffer.NativePtr != IntPtr.Zero)
+ {
+ var renderCommandEncoder = GetOrCreateRenderEncoder(true);
+
+ renderCommandEncoder.DrawIndexedPrimitives(
+ primitiveType,
+ type,
+ indexBuffer,
+ (ulong)indexOffset,
+ buffer,
+ (ulong)(indirectBuffer.Offset + offset));
+ }
+ }
+
+ public void DrawIndexedIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
+ {
+ for (int i = 0; i < maxDrawCount; i++)
+ {
+ DrawIndexedIndirectOffset(indirectBuffer, stride * i);
+ }
+ }
+
+ public void DrawIndirect(BufferRange indirectBuffer)
+ {
+ DrawIndirectOffset(indirectBuffer);
+ }
+
+ public void DrawIndirectOffset(BufferRange indirectBuffer, int offset = 0)
+ {
+ if (TopologyUnsupported(_encoderStateManager.Topology))
+ {
+ // TODO: Reindex unsupported topologies
+ Logger.Warning?.Print(LogClass.Gpu, $"Drawing indirect with unsupported topology: {_encoderStateManager.Topology}");
+ }
+
+ var buffer = _renderer.BufferManager
+ .GetBuffer(indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false)
+ .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
+
+ var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert();
+ var renderCommandEncoder = GetOrCreateRenderEncoder(true);
+
+ renderCommandEncoder.DrawPrimitives(
+ primitiveType,
+ buffer,
+ (ulong)(indirectBuffer.Offset + offset));
+ }
+
+ public void DrawIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
+ {
+ for (int i = 0; i < maxDrawCount; i++)
+ {
+ DrawIndirectOffset(indirectBuffer, stride * i);
+ }
+ }
+
+ public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion)
+ {
+ _renderer.HelperShader.DrawTexture(texture, sampler, srcRegion, dstRegion);
+ }
+
+ public void SetAlphaTest(bool enable, float reference, CompareOp op)
+ {
+ // This is currently handled using shader specialization, as Metal does not support alpha test.
+ // In the future, we may want to use this to write the reference value into the support buffer,
+ // to avoid creating one version of the shader per reference value used.
+ }
+
+ public void SetBlendState(AdvancedBlendDescriptor blend)
+ {
+ // Metal does not support advanced blend.
+ }
+
+ public void SetBlendState(int index, BlendDescriptor blend)
+ {
+ _encoderStateManager.UpdateBlendDescriptors(index, blend);
+ }
+
+ public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
+ {
+ if (enables == 0)
+ {
+ _encoderStateManager.UpdateDepthBias(0, 0, 0);
+ }
+ else
+ {
+ _encoderStateManager.UpdateDepthBias(units, factor, clamp);
+ }
+ }
+
+ public void SetDepthClamp(bool clamp)
+ {
+ _encoderStateManager.UpdateDepthClamp(clamp);
+ }
+
+ public void SetDepthMode(DepthMode mode)
+ {
+ // Metal does not support depth clip control.
+ }
+
+ public void SetDepthTest(DepthTestDescriptor depthTest)
+ {
+ _encoderStateManager.UpdateDepthState(depthTest);
+ }
+
+ public void SetFaceCulling(bool enable, Face face)
+ {
+ _encoderStateManager.UpdateCullMode(enable, face);
+ }
+
+ public void SetFrontFace(FrontFace frontFace)
+ {
+ _encoderStateManager.UpdateFrontFace(frontFace);
+ }
+
+ public void SetIndexBuffer(BufferRange buffer, IndexType type)
+ {
+ _encoderStateManager.UpdateIndexBuffer(buffer, type);
+ }
+
+ public void SetImage(ShaderStage stage, int binding, ITexture image)
+ {
+ if (image is TextureBase img)
+ {
+ _encoderStateManager.UpdateImage(stage, binding, img);
+ }
+ }
+
+ public void SetImageArray(ShaderStage stage, int binding, IImageArray array)
+ {
+ if (array is ImageArray imageArray)
+ {
+ _encoderStateManager.UpdateImageArray(stage, binding, imageArray);
+ }
+ }
+
+ public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array)
+ {
+ if (array is ImageArray imageArray)
+ {
+ _encoderStateManager.UpdateImageArraySeparate(stage, setIndex, imageArray);
+ }
+ }
+
+ public void SetLineParameters(float width, bool smooth)
+ {
+ // Metal does not support wide-lines.
+ }
+
+ public void SetLogicOpState(bool enable, LogicalOp op)
+ {
+ _encoderStateManager.UpdateLogicOpState(enable, op);
+ }
+
+ public void SetMultisampleState(MultisampleDescriptor multisample)
+ {
+ _encoderStateManager.UpdateMultisampleState(multisample);
+ }
+
+ public void SetPatchParameters(int vertices, ReadOnlySpan defaultOuterLevel, ReadOnlySpan defaultInnerLevel)
+ {
+ Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!");
+ }
+
+ public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
+ {
+ Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!");
+ }
+
+ public void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode)
+ {
+ // Metal does not support polygon mode.
+ }
+
+ public void SetPrimitiveRestart(bool enable, int index)
+ {
+ // Always active for LineStrip and TriangleStrip
+ // https://github.com/gpuweb/gpuweb/issues/1220#issuecomment-732483263
+ // https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515520-drawindexedprimitives
+ // https://stackoverflow.com/questions/70813665/how-to-render-multiple-trianglestrips-using-metal
+
+ // Emulating disabling this is very difficult. It's unlikely for an index buffer to use the largest possible index,
+ // so it's fine nearly all of the time.
+ }
+
+ public void SetPrimitiveTopology(PrimitiveTopology topology)
+ {
+ _encoderStateManager.UpdatePrimitiveTopology(topology);
+ }
+
+ public void SetProgram(IProgram program)
+ {
+ _encoderStateManager.UpdateProgram(program);
+ }
+
+ public void SetRasterizerDiscard(bool discard)
+ {
+ _encoderStateManager.UpdateRasterizerDiscard(discard);
+ }
+
+ public void SetRenderTargetColorMasks(ReadOnlySpan componentMask)
+ {
+ _encoderStateManager.UpdateRenderTargetColorMasks(componentMask);
+ }
+
+ public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
+ {
+ _encoderStateManager.UpdateRenderTargets(colors, depthStencil);
+ }
+
+ public void SetScissors(ReadOnlySpan> regions)
+ {
+ _encoderStateManager.UpdateScissors(regions);
+ }
+
+ public void SetStencilTest(StencilTestDescriptor stencilTest)
+ {
+ _encoderStateManager.UpdateStencilState(stencilTest);
+ }
+
+ public void SetUniformBuffers(ReadOnlySpan buffers)
+ {
+ _encoderStateManager.UpdateUniformBuffers(buffers);
+ }
+
+ public void SetStorageBuffers(ReadOnlySpan buffers)
+ {
+ _encoderStateManager.UpdateStorageBuffers(buffers);
+ }
+
+ internal void SetStorageBuffers(int first, ReadOnlySpan> buffers)
+ {
+ _encoderStateManager.UpdateStorageBuffers(first, buffers);
+ }
+
+ public void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler)
+ {
+ if (texture is TextureBase tex)
+ {
+ if (sampler == null || sampler is SamplerHolder)
+ {
+ _encoderStateManager.UpdateTextureAndSampler(stage, binding, tex, (SamplerHolder)sampler);
+ }
+ }
+ }
+
+ public void SetTextureArray(ShaderStage stage, int binding, ITextureArray array)
+ {
+ if (array is TextureArray textureArray)
+ {
+ _encoderStateManager.UpdateTextureArray(stage, binding, textureArray);
+ }
+ }
+
+ public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array)
+ {
+ if (array is TextureArray textureArray)
+ {
+ _encoderStateManager.UpdateTextureArraySeparate(stage, setIndex, textureArray);
+ }
+ }
+
+ public void SetUserClipDistance(int index, bool enableClip)
+ {
+ // TODO. Same as Vulkan
+ }
+
+ public void SetVertexAttribs(ReadOnlySpan vertexAttribs)
+ {
+ _encoderStateManager.UpdateVertexAttribs(vertexAttribs);
+ }
+
+ public void SetVertexBuffers(ReadOnlySpan vertexBuffers)
+ {
+ _encoderStateManager.UpdateVertexBuffers(vertexBuffers);
+ }
+
+ public void SetViewports(ReadOnlySpan viewports)
+ {
+ _encoderStateManager.UpdateViewports(viewports);
+ }
+
+ public void TextureBarrier()
+ {
+ if (CurrentEncoderType == EncoderType.Render)
+ {
+ Encoders.RenderEncoder.MemoryBarrier(MTLBarrierScope.Textures, MTLRenderStages.RenderStageFragment, MTLRenderStages.RenderStageFragment);
+ }
+ }
+
+ public void TextureBarrierTiled()
+ {
+ TextureBarrier();
+ }
+
+ public bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual)
+ {
+ // TODO: Implementable via indirect draw commands
+ return false;
+ }
+
+ public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual)
+ {
+ // TODO: Implementable via indirect draw commands
+ return false;
+ }
+
+ public void EndHostConditionalRendering()
+ {
+ // TODO: Implementable via indirect draw commands
+ }
+
+ public void BeginTransformFeedback(PrimitiveTopology topology)
+ {
+ // Metal does not support transform feedback.
+ }
+
+ public void EndTransformFeedback()
+ {
+ // Metal does not support transform feedback.
+ }
+
+ public void SetTransformFeedbackBuffers(ReadOnlySpan buffers)
+ {
+ // Metal does not support transform feedback.
+ }
+
+ public void Dispose()
+ {
+ EndCurrentPass();
+ _encoderStateManager.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Program.cs b/src/Ryujinx.Graphics.Metal/Program.cs
new file mode 100644
index 000000000..37bae5817
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Program.cs
@@ -0,0 +1,286 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using SharpMetal.Foundation;
+using SharpMetal.Metal;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class Program : IProgram
+ {
+ private ProgramLinkStatus _status;
+ private readonly ShaderSource[] _shaders;
+ private readonly GCHandle[] _handles;
+ private int _successCount;
+
+ private readonly MetalRenderer _renderer;
+
+ public MTLFunction VertexFunction;
+ public MTLFunction FragmentFunction;
+ public MTLFunction ComputeFunction;
+ public ComputeSize ComputeLocalSize { get; }
+
+ private HashTableSlim _graphicsPipelineCache;
+ private MTLComputePipelineState? _computePipelineCache;
+ private bool _firstBackgroundUse;
+
+ public ResourceBindingSegment[][] BindingSegments { get; }
+ // Argument buffer sizes for Vertex or Compute stages
+ public int[] ArgumentBufferSizes { get; }
+ // Argument buffer sizes for Fragment stage
+ public int[] FragArgumentBufferSizes { get; }
+
+ public Program(
+ MetalRenderer renderer,
+ MTLDevice device,
+ ShaderSource[] shaders,
+ ResourceLayout resourceLayout,
+ ComputeSize computeLocalSize = default)
+ {
+ _renderer = renderer;
+ renderer.Programs.Add(this);
+
+ ComputeLocalSize = computeLocalSize;
+ _shaders = shaders;
+ _handles = new GCHandle[_shaders.Length];
+
+ _status = ProgramLinkStatus.Incomplete;
+
+ for (int i = 0; i < _shaders.Length; i++)
+ {
+ ShaderSource shader = _shaders[i];
+
+ using var compileOptions = new MTLCompileOptions
+ {
+ PreserveInvariance = true,
+ LanguageVersion = MTLLanguageVersion.Version31,
+ };
+ var index = i;
+
+ _handles[i] = device.NewLibrary(StringHelper.NSString(shader.Code), compileOptions, (library, error) => CompilationResultHandler(library, error, index));
+ }
+
+ (BindingSegments, ArgumentBufferSizes, FragArgumentBufferSizes) = BuildBindingSegments(resourceLayout.SetUsages);
+ }
+
+ public void CompilationResultHandler(MTLLibrary library, NSError error, int index)
+ {
+ var shader = _shaders[index];
+
+ if (_handles[index].IsAllocated)
+ {
+ _handles[index].Free();
+ }
+
+ if (error != IntPtr.Zero)
+ {
+ Logger.Warning?.PrintMsg(LogClass.Gpu, shader.Code);
+ Logger.Warning?.Print(LogClass.Gpu, $"{shader.Stage} shader linking failed: \n{StringHelper.String(error.LocalizedDescription)}");
+ _status = ProgramLinkStatus.Failure;
+ return;
+ }
+
+ switch (shader.Stage)
+ {
+ case ShaderStage.Compute:
+ ComputeFunction = library.NewFunction(StringHelper.NSString("kernelMain"));
+ break;
+ case ShaderStage.Vertex:
+ VertexFunction = library.NewFunction(StringHelper.NSString("vertexMain"));
+ break;
+ case ShaderStage.Fragment:
+ FragmentFunction = library.NewFunction(StringHelper.NSString("fragmentMain"));
+ break;
+ default:
+ Logger.Warning?.Print(LogClass.Gpu, $"Cannot handle stage {shader.Stage}!");
+ break;
+ }
+
+ _successCount++;
+
+ if (_successCount >= _shaders.Length && _status != ProgramLinkStatus.Failure)
+ {
+ _status = ProgramLinkStatus.Success;
+ }
+ }
+
+ private static (ResourceBindingSegment[][], int[], int[]) BuildBindingSegments(ReadOnlyCollection setUsages)
+ {
+ ResourceBindingSegment[][] segments = new ResourceBindingSegment[setUsages.Count][];
+ int[] argBufferSizes = new int[setUsages.Count];
+ int[] fragArgBufferSizes = new int[setUsages.Count];
+
+ for (int setIndex = 0; setIndex < setUsages.Count; setIndex++)
+ {
+ List currentSegments = new();
+
+ ResourceUsage currentUsage = default;
+ int currentCount = 0;
+
+ for (int index = 0; index < setUsages[setIndex].Usages.Count; index++)
+ {
+ ResourceUsage usage = setUsages[setIndex].Usages[index];
+
+ if (currentUsage.Binding + currentCount != usage.Binding ||
+ currentUsage.Type != usage.Type ||
+ currentUsage.Stages != usage.Stages ||
+ currentUsage.ArrayLength > 1 ||
+ usage.ArrayLength > 1)
+ {
+ if (currentCount != 0)
+ {
+ currentSegments.Add(new ResourceBindingSegment(
+ currentUsage.Binding,
+ currentCount,
+ currentUsage.Type,
+ currentUsage.Stages,
+ currentUsage.ArrayLength > 1));
+
+ var size = currentCount * ResourcePointerSize(currentUsage.Type);
+ if (currentUsage.Stages.HasFlag(ResourceStages.Fragment))
+ {
+ fragArgBufferSizes[setIndex] += size;
+ }
+
+ if (currentUsage.Stages.HasFlag(ResourceStages.Vertex) ||
+ currentUsage.Stages.HasFlag(ResourceStages.Compute))
+ {
+ argBufferSizes[setIndex] += size;
+ }
+ }
+
+ currentUsage = usage;
+ currentCount = usage.ArrayLength;
+ }
+ else
+ {
+ currentCount++;
+ }
+ }
+
+ if (currentCount != 0)
+ {
+ currentSegments.Add(new ResourceBindingSegment(
+ currentUsage.Binding,
+ currentCount,
+ currentUsage.Type,
+ currentUsage.Stages,
+ currentUsage.ArrayLength > 1));
+
+ var size = currentCount * ResourcePointerSize(currentUsage.Type);
+ if (currentUsage.Stages.HasFlag(ResourceStages.Fragment))
+ {
+ fragArgBufferSizes[setIndex] += size;
+ }
+
+ if (currentUsage.Stages.HasFlag(ResourceStages.Vertex) ||
+ currentUsage.Stages.HasFlag(ResourceStages.Compute))
+ {
+ argBufferSizes[setIndex] += size;
+ }
+ }
+
+ segments[setIndex] = currentSegments.ToArray();
+ }
+
+ return (segments, argBufferSizes, fragArgBufferSizes);
+ }
+
+ private static int ResourcePointerSize(ResourceType type)
+ {
+ return (type == ResourceType.TextureAndSampler ? 2 : 1);
+ }
+
+ public ProgramLinkStatus CheckProgramLink(bool blocking)
+ {
+ if (blocking)
+ {
+ while (_status == ProgramLinkStatus.Incomplete)
+ { }
+
+ return _status;
+ }
+
+ return _status;
+ }
+
+ public byte[] GetBinary()
+ {
+ return [];
+ }
+
+ public void AddGraphicsPipeline(ref PipelineUid key, MTLRenderPipelineState pipeline)
+ {
+ (_graphicsPipelineCache ??= new()).Add(ref key, pipeline);
+ }
+
+ public void AddComputePipeline(MTLComputePipelineState pipeline)
+ {
+ _computePipelineCache = pipeline;
+ }
+
+ public bool TryGetGraphicsPipeline(ref PipelineUid key, out MTLRenderPipelineState pipeline)
+ {
+ if (_graphicsPipelineCache == null)
+ {
+ pipeline = default;
+ return false;
+ }
+
+ if (!_graphicsPipelineCache.TryGetValue(ref key, out pipeline))
+ {
+ if (_firstBackgroundUse)
+ {
+ Logger.Warning?.Print(LogClass.Gpu, "Background pipeline compile missed on draw - incorrect pipeline state?");
+ _firstBackgroundUse = false;
+ }
+
+ return false;
+ }
+
+ _firstBackgroundUse = false;
+
+ return true;
+ }
+
+ public bool TryGetComputePipeline(out MTLComputePipelineState pipeline)
+ {
+ if (_computePipelineCache.HasValue)
+ {
+ pipeline = _computePipelineCache.Value;
+ return true;
+ }
+
+ pipeline = default;
+ return false;
+ }
+
+ public void Dispose()
+ {
+ if (!_renderer.Programs.Remove(this))
+ {
+ return;
+ }
+
+ if (_graphicsPipelineCache != null)
+ {
+ foreach (MTLRenderPipelineState pipeline in _graphicsPipelineCache.Values)
+ {
+ pipeline.Dispose();
+ }
+ }
+
+ _computePipelineCache?.Dispose();
+
+ VertexFunction.Dispose();
+ FragmentFunction.Dispose();
+ ComputeFunction.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs b/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs
new file mode 100644
index 000000000..8e6d88c4b
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs
@@ -0,0 +1,22 @@
+using Ryujinx.Graphics.GAL;
+
+namespace Ryujinx.Graphics.Metal
+{
+ readonly struct ResourceBindingSegment
+ {
+ public readonly int Binding;
+ public readonly int Count;
+ public readonly ResourceType Type;
+ public readonly ResourceStages Stages;
+ public readonly bool IsArray;
+
+ public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages, bool isArray)
+ {
+ Binding = binding;
+ Count = count;
+ Type = type;
+ Stages = stages;
+ IsArray = isArray;
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs b/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs
new file mode 100644
index 000000000..36ae9bac6
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs
@@ -0,0 +1,59 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class ResourceLayoutBuilder
+ {
+ private const int TotalSets = MetalRenderer.TotalSets;
+
+ private readonly List[] _resourceDescriptors;
+ private readonly List[] _resourceUsages;
+
+ public ResourceLayoutBuilder()
+ {
+ _resourceDescriptors = new List[TotalSets];
+ _resourceUsages = new List[TotalSets];
+
+ for (int index = 0; index < TotalSets; index++)
+ {
+ _resourceDescriptors[index] = new();
+ _resourceUsages[index] = new();
+ }
+ }
+
+ public ResourceLayoutBuilder Add(ResourceStages stages, ResourceType type, int binding, bool write = false)
+ {
+ uint setIndex = type switch
+ {
+ ResourceType.UniformBuffer => Constants.ConstantBuffersSetIndex,
+ ResourceType.StorageBuffer => Constants.StorageBuffersSetIndex,
+ ResourceType.TextureAndSampler or ResourceType.BufferTexture => Constants.TexturesSetIndex,
+ ResourceType.Image or ResourceType.BufferImage => Constants.ImagesSetIndex,
+ _ => throw new ArgumentException($"Invalid resource type \"{type}\"."),
+ };
+
+ _resourceDescriptors[setIndex].Add(new ResourceDescriptor(binding, 1, type, stages));
+ _resourceUsages[setIndex].Add(new ResourceUsage(binding, 1, type, stages, write));
+
+ return this;
+ }
+
+ public ResourceLayout Build()
+ {
+ var descriptors = new ResourceDescriptorCollection[TotalSets];
+ var usages = new ResourceUsageCollection[TotalSets];
+
+ for (int index = 0; index < TotalSets; index++)
+ {
+ descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly());
+ usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly());
+ }
+
+ return new ResourceLayout(descriptors.AsReadOnly(), usages.AsReadOnly());
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj b/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj
new file mode 100644
index 000000000..02afb150a
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj
@@ -0,0 +1,30 @@
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.Graphics.Metal/SamplerHolder.cs b/src/Ryujinx.Graphics.Metal/SamplerHolder.cs
new file mode 100644
index 000000000..3241efa6d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/SamplerHolder.cs
@@ -0,0 +1,90 @@
+using Ryujinx.Graphics.GAL;
+using SharpMetal.Metal;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ class SamplerHolder : ISampler
+ {
+ private readonly MetalRenderer _renderer;
+ private readonly Auto _sampler;
+
+ public SamplerHolder(MetalRenderer renderer, MTLDevice device, SamplerCreateInfo info)
+ {
+ _renderer = renderer;
+
+ renderer.Samplers.Add(this);
+
+ (MTLSamplerMinMagFilter minFilter, MTLSamplerMipFilter mipFilter) = info.MinFilter.Convert();
+
+ MTLSamplerBorderColor borderColor = GetConstrainedBorderColor(info.BorderColor, out _);
+
+ using var descriptor = new MTLSamplerDescriptor
+ {
+ BorderColor = borderColor,
+ MinFilter = minFilter,
+ MagFilter = info.MagFilter.Convert(),
+ MipFilter = mipFilter,
+ CompareFunction = info.CompareOp.Convert(),
+ LodMinClamp = info.MinLod,
+ LodMaxClamp = info.MaxLod,
+ LodAverage = false,
+ MaxAnisotropy = Math.Max((uint)info.MaxAnisotropy, 1),
+ SAddressMode = info.AddressU.Convert(),
+ TAddressMode = info.AddressV.Convert(),
+ RAddressMode = info.AddressP.Convert(),
+ SupportArgumentBuffers = true
+ };
+
+ var sampler = device.NewSamplerState(descriptor);
+
+ _sampler = new Auto(new DisposableSampler(sampler));
+ }
+
+ private static MTLSamplerBorderColor GetConstrainedBorderColor(ColorF arbitraryBorderColor, out bool cantConstrain)
+ {
+ float r = arbitraryBorderColor.Red;
+ float g = arbitraryBorderColor.Green;
+ float b = arbitraryBorderColor.Blue;
+ float a = arbitraryBorderColor.Alpha;
+
+ if (r == 0f && g == 0f && b == 0f)
+ {
+ if (a == 1f)
+ {
+ cantConstrain = false;
+ return MTLSamplerBorderColor.OpaqueBlack;
+ }
+
+ if (a == 0f)
+ {
+ cantConstrain = false;
+ return MTLSamplerBorderColor.TransparentBlack;
+ }
+ }
+ else if (r == 1f && g == 1f && b == 1f && a == 1f)
+ {
+ cantConstrain = false;
+ return MTLSamplerBorderColor.OpaqueWhite;
+ }
+
+ cantConstrain = true;
+ return MTLSamplerBorderColor.OpaqueBlack;
+ }
+
+ public Auto GetSampler()
+ {
+ return _sampler;
+ }
+
+ public void Dispose()
+ {
+ if (_renderer.Samplers.Remove(this))
+ {
+ _sampler.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal
new file mode 100644
index 000000000..887878499
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal
@@ -0,0 +1,43 @@
+#include
+
+using namespace metal;
+
+struct CopyVertexOut {
+ float4 position [[position]];
+ float2 uv;
+};
+
+struct TexCoords {
+ float data[4];
+};
+
+struct ConstantBuffers {
+ constant TexCoords* tex_coord;
+};
+
+struct Textures
+{
+ texture2d texture;
+ sampler sampler;
+};
+
+vertex CopyVertexOut vertexMain(uint vid [[vertex_id]],
+ constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
+ CopyVertexOut out;
+
+ int low = vid & 1;
+ int high = vid >> 1;
+ out.uv.x = constant_buffers.tex_coord->data[low];
+ out.uv.y = constant_buffers.tex_coord->data[2 + high];
+ out.position.x = (float(low) - 0.5f) * 2.0f;
+ out.position.y = (float(high) - 0.5f) * 2.0f;
+ out.position.z = 0.0f;
+ out.position.w = 1.0f;
+
+ return out;
+}
+
+fragment FORMAT4 fragmentMain(CopyVertexOut in [[stage_in]],
+ constant Textures &textures [[buffer(TEXTURES_INDEX)]]) {
+ return textures.texture.sample(textures.sampler, in.uv);
+}
diff --git a/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal b/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal
new file mode 100644
index 000000000..1077b6cea
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal
@@ -0,0 +1,45 @@
+#include
+
+using namespace metal;
+
+struct CopyVertexOut {
+ float4 position [[position]];
+ float2 uv;
+};
+
+struct TexCoords {
+ float data[4];
+};
+
+struct ConstantBuffers {
+ constant TexCoords* tex_coord;
+};
+
+struct Textures
+{
+ texture2d_ms texture;
+};
+
+vertex CopyVertexOut vertexMain(uint vid [[vertex_id]],
+ constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
+ CopyVertexOut out;
+
+ int low = vid & 1;
+ int high = vid >> 1;
+ out.uv.x = constant_buffers.tex_coord->data[low];
+ out.uv.y = constant_buffers.tex_coord->data[2 + high];
+ out.position.x = (float(low) - 0.5f) * 2.0f;
+ out.position.y = (float(high) - 0.5f) * 2.0f;
+ out.position.z = 0.0f;
+ out.position.w = 1.0f;
+
+ return out;
+}
+
+fragment FORMAT4 fragmentMain(CopyVertexOut in [[stage_in]],
+ constant Textures &textures [[buffer(TEXTURES_INDEX)]],
+ uint sample_id [[sample_id]]) {
+ uint2 tex_size = uint2(textures.texture.get_width(), textures.texture.get_height());
+ uint2 tex_coord = uint2(in.uv * float2(tex_size));
+ return textures.texture.read(tex_coord, sample_id);
+}
diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal b/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal
new file mode 100644
index 000000000..1a7d2c574
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal
@@ -0,0 +1,72 @@
+#include
+
+using namespace metal;
+
+struct StrideArguments {
+ int4 data;
+};
+
+struct InData {
+ uint8_t data[1];
+};
+
+struct OutData {
+ uint8_t data[1];
+};
+
+struct ConstantBuffers {
+ constant StrideArguments* stride_arguments;
+};
+
+struct StorageBuffers {
+ device InData* in_data;
+ device OutData* out_data;
+};
+
+kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]],
+ device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]],
+ uint3 thread_position_in_grid [[thread_position_in_grid]],
+ uint3 threads_per_threadgroup [[threads_per_threadgroup]],
+ uint3 threadgroups_per_grid [[threadgroups_per_grid]])
+{
+ // Determine what slice of the stride copies this invocation will perform.
+
+ int sourceStride = constant_buffers.stride_arguments->data.x;
+ int targetStride = constant_buffers.stride_arguments->data.y;
+ int bufferSize = constant_buffers.stride_arguments->data.z;
+ int sourceOffset = constant_buffers.stride_arguments->data.w;
+
+ int strideRemainder = targetStride - sourceStride;
+ int invocations = int(threads_per_threadgroup.x * threadgroups_per_grid.x);
+
+ int copiesRequired = bufferSize / sourceStride;
+
+ // Find the copies that this invocation should perform.
+
+ // - Copies that all invocations perform.
+ int allInvocationCopies = copiesRequired / invocations;
+
+ // - Extra remainder copy that this invocation performs.
+ int index = int(thread_position_in_grid.x);
+ int extra = (index < (copiesRequired % invocations)) ? 1 : 0;
+
+ int copyCount = allInvocationCopies + extra;
+
+ // Finally, get the starting offset. Make sure to count extra copies.
+
+ int startCopy = allInvocationCopies * index + min(copiesRequired % invocations, index);
+
+ int srcOffset = sourceOffset + startCopy * sourceStride;
+ int dstOffset = startCopy * targetStride;
+
+ // Perform the copies for this region
+ for (int i = 0; i < copyCount; i++) {
+ for (int j = 0; j < sourceStride; j++) {
+ storage_buffers.out_data->data[dstOffset++] = storage_buffers.in_data->data[srcOffset++];
+ }
+
+ for (int j = 0; j < strideRemainder; j++) {
+ storage_buffers.out_data->data[dstOffset++] = uint8_t(0);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal b/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal
new file mode 100644
index 000000000..46a57e035
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal
@@ -0,0 +1,38 @@
+#include
+
+using namespace metal;
+
+struct VertexOut {
+ float4 position [[position]];
+};
+
+struct ClearColor {
+ FORMAT4 data;
+};
+
+struct ConstantBuffers {
+ constant ClearColor* clear_color;
+};
+
+vertex VertexOut vertexMain(ushort vid [[vertex_id]]) {
+ int low = vid & 1;
+ int high = vid >> 1;
+
+ VertexOut out;
+
+ out.position.x = (float(low) - 0.5f) * 2.0f;
+ out.position.y = (float(high) - 0.5f) * 2.0f;
+ out.position.z = 0.0f;
+ out.position.w = 1.0f;
+
+ return out;
+}
+
+struct FragmentOut {
+ FORMAT4 color [[color(COLOR_ATTACHMENT_INDEX)]];
+};
+
+fragment FragmentOut fragmentMain(VertexOut in [[stage_in]],
+ constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
+ return {constant_buffers.clear_color->data};
+}
diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal b/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal
new file mode 100644
index 000000000..870ac3d78
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal
@@ -0,0 +1,66 @@
+#include
+
+using namespace metal;
+
+struct StrideArguments {
+ int pixelCount;
+ int dstStartOffset;
+};
+
+struct InData {
+ uint data[1];
+};
+
+struct OutData {
+ uint data[1];
+};
+
+struct ConstantBuffers {
+ constant StrideArguments* stride_arguments;
+};
+
+struct StorageBuffers {
+ device InData* in_data;
+ device OutData* out_data;
+};
+
+kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]],
+ device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]],
+ uint3 thread_position_in_grid [[thread_position_in_grid]],
+ uint3 threads_per_threadgroup [[threads_per_threadgroup]],
+ uint3 threadgroups_per_grid [[threadgroups_per_grid]])
+{
+ // Determine what slice of the stride copies this invocation will perform.
+ int invocations = int(threads_per_threadgroup.x * threadgroups_per_grid.x);
+
+ int copiesRequired = constant_buffers.stride_arguments->pixelCount;
+
+ // Find the copies that this invocation should perform.
+
+ // - Copies that all invocations perform.
+ int allInvocationCopies = copiesRequired / invocations;
+
+ // - Extra remainder copy that this invocation performs.
+ int index = int(thread_position_in_grid.x);
+ int extra = (index < (copiesRequired % invocations)) ? 1 : 0;
+
+ int copyCount = allInvocationCopies + extra;
+
+ // Finally, get the starting offset. Make sure to count extra copies.
+
+ int startCopy = allInvocationCopies * index + min(copiesRequired % invocations, index);
+
+ int srcOffset = startCopy * 2;
+ int dstOffset = constant_buffers.stride_arguments->dstStartOffset + startCopy;
+
+ // Perform the conversion for this region.
+ for (int i = 0; i < copyCount; i++)
+ {
+ float depth = as_type