diff --git a/Directory.Packages.props b/Directory.Packages.props
index 301024cf8a..905f7847d1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -38,6 +38,7 @@
+
diff --git a/Ryujinx.sln b/Ryujinx.sln
index 76ebd573f3..9b6d3ce585 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -88,6 +88,10 @@ 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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -255,6 +259,10 @@ 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
+ {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/src/Ryujinx.Common/Configuration/GraphicsBackend.cs b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
index e3b4f91b01..ba68fefbe1 100644
--- a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
+++ b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs
@@ -8,5 +8,6 @@ namespace Ryujinx.Common.Configuration
{
Vulkan,
OpenGl,
+ Metal
}
}
diff --git a/src/Ryujinx.Graphics.Metal/Constants.cs b/src/Ryujinx.Graphics.Metal/Constants.cs
new file mode 100644
index 0000000000..06fd84a523
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Constants.cs
@@ -0,0 +1,13 @@
+namespace Ryujinx.Graphics.Metal
+{
+ static class Constants
+ {
+ // TODO: Check these values, these were largely copied from Vulkan
+ public const int MaxShaderStages = 5;
+ public const int MaxUniformBuffersPerStage = 18;
+ public const int MaxStorageBuffersPerStage = 16;
+ public const int MaxTexturesPerStage = 64;
+ public const int MaxCommandBuffersPerQueue = 16;
+ public const int MaxTextureBindings = MaxTexturesPerStage * MaxShaderStages;
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/EnumConversion.cs b/src/Ryujinx.Graphics.Metal/EnumConversion.cs
new file mode 100644
index 0000000000..120c2d71ee
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/EnumConversion.cs
@@ -0,0 +1,192 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using SharpMetal;
+
+namespace Ryujinx.Graphics.Metal
+{
+ 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)
+ {
+ return frontFace switch
+ {
+ FrontFace.Clockwise => MTLWinding.Clockwise,
+ FrontFace.CounterClockwise => MTLWinding.CounterClockwise,
+ _ => 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 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))
+
+ };
+ }
+
+ // TODO: Metal does not have native support for Triangle Fans but it is possible to emulate with TriangleStrip and moving around the indices
+ 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.TypeTextureBuffer,
+ 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.TypeCube,
+ Target.CubemapArray => MTLTextureType.TypeCubeArray,
+ _ => LogInvalidAndReturn(target, nameof(Target), MTLTextureType.Type2D)
+ };
+ }
+
+ private static T2 LogInvalidAndReturn(T1 value, string name, T2 defaultValue = default)
+ {
+ Logger.Debug?.Print(LogClass.Gpu, $"Invalid {name} enum value: {value}.");
+
+ return defaultValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/FormatCapabilities.cs b/src/Ryujinx.Graphics.Metal/FormatCapabilities.cs
new file mode 100644
index 0000000000..6dc56c59b2
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/FormatCapabilities.cs
@@ -0,0 +1,14 @@
+using SharpMetal;
+
+namespace Ryujinx.Graphics.Metal
+{
+ static class FormatCapabilities
+ {
+ public static MTLPixelFormat ConvertToMTLFormat(GAL.Format srcFormat)
+ {
+ var format = FormatTable.GetFormat(srcFormat);
+
+ return format;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/FormatTable.cs b/src/Ryujinx.Graphics.Metal/FormatTable.cs
new file mode 100644
index 0000000000..a21271e1cb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/FormatTable.cs
@@ -0,0 +1,172 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using SharpMetal;
+
+namespace Ryujinx.Graphics.Metal
+{
+ 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.S8UintD24Unorm);
+ Add(Format.D32Float, MTLPixelFormat.Depth32Float);
+ Add(Format.D24UnormS8Uint, MTLPixelFormat.Depth24Unorm_Stencil8);
+ Add(Format.D32FloatS8Uint, MTLPixelFormat.Depth32Float_Stencil8);
+ Add(Format.R8G8B8A8Srgb, MTLPixelFormat.RGBA8Unorm_sRGB);
+ // Add(Format.R4G4Unorm, MTLPixelFormat.R4G4Unorm);
+ // Add(Format.R4G4B4A4Unorm, MTLPixelFormat.R4G4B4A4Unorm);
+ // Add(Format.R5G5B5X1Unorm, MTLPixelFormat.R5G5B5X1Unorm);
+ // Add(Format.R5G5B5A1Unorm, MTLPixelFormat.R5G5B5A1Unorm);
+ 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.BC1_RGBA);
+ Add(Format.Bc2Unorm, MTLPixelFormat.BC2_RGBA);
+ Add(Format.Bc3Unorm, MTLPixelFormat.BC3_RGBA);
+ Add(Format.Bc1RgbaSrgb, MTLPixelFormat.BC1_RGBA_sRGB);
+ Add(Format.Bc2Srgb, MTLPixelFormat.BC2_RGBA_sRGB);
+ Add(Format.Bc3Srgb, MTLPixelFormat.BC3_RGBA_sRGB);
+ Add(Format.Bc4Unorm, MTLPixelFormat.BC4_RUnorm);
+ Add(Format.Bc4Snorm, MTLPixelFormat.BC4_RSnorm);
+ Add(Format.Bc5Unorm, MTLPixelFormat.BC5_RGUnorm);
+ Add(Format.Bc5Snorm, MTLPixelFormat.BC5_RGSnorm);
+ Add(Format.Bc7Unorm, MTLPixelFormat.BC7_RGBAUnorm);
+ Add(Format.Bc7Srgb, MTLPixelFormat.BC7_RGBAUnorm_sRGB);
+ Add(Format.Bc6HSfloat, MTLPixelFormat.BC6H_RGBFloat);
+ Add(Format.Bc6HUfloat, MTLPixelFormat.BC6H_RGBUfloat);
+ Add(Format.Etc2RgbUnorm, MTLPixelFormat.ETC2_RGB8);
+ Add(Format.Etc2RgbaUnorm, MTLPixelFormat.ETC2_RGB8A1);
+ // Add(Format.Etc2RgbPtaUnorm, MTLPixelFormat.Etc2RgbPtaUnorm);
+ Add(Format.Etc2RgbSrgb, MTLPixelFormat.ETC2_RGB8_sRGB);
+ Add(Format.Etc2RgbaSrgb, MTLPixelFormat.ETC2_RGB8A1_sRGB);
+ // Add(Format.Etc2RgbPtaSrgb, MTLPixelFormat.Etc2RgbPtaSrgb);
+ // 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.ASTC_4x4_LDR);
+ Add(Format.Astc5x4Unorm, MTLPixelFormat.ASTC_5x4_LDR);
+ Add(Format.Astc5x5Unorm, MTLPixelFormat.ASTC_5x5_LDR);
+ Add(Format.Astc6x5Unorm, MTLPixelFormat.ASTC_6x5_LDR);
+ Add(Format.Astc6x6Unorm, MTLPixelFormat.ASTC_6x6_LDR);
+ Add(Format.Astc8x5Unorm, MTLPixelFormat.ASTC_8x5_LDR);
+ Add(Format.Astc8x6Unorm, MTLPixelFormat.ASTC_8x6_LDR);
+ Add(Format.Astc8x8Unorm, MTLPixelFormat.ASTC_8x8_LDR);
+ Add(Format.Astc10x5Unorm, MTLPixelFormat.ASTC_10x5_LDR);
+ Add(Format.Astc10x6Unorm, MTLPixelFormat.ASTC_10x6_LDR);
+ Add(Format.Astc10x8Unorm, MTLPixelFormat.ASTC_10x8_LDR);
+ Add(Format.Astc10x10Unorm, MTLPixelFormat.ASTC_10x10_LDR);
+ Add(Format.Astc12x10Unorm, MTLPixelFormat.ASTC_12x10_LDR);
+ Add(Format.Astc12x12Unorm, MTLPixelFormat.ASTC_12x12_LDR);
+ Add(Format.Astc4x4Srgb, MTLPixelFormat.ASTC_4x4_sRGB);
+ Add(Format.Astc5x4Srgb, MTLPixelFormat.ASTC_5x4_sRGB);
+ Add(Format.Astc5x5Srgb, MTLPixelFormat.ASTC_5x5_sRGB);
+ Add(Format.Astc6x5Srgb, MTLPixelFormat.ASTC_6x5_sRGB);
+ Add(Format.Astc6x6Srgb, MTLPixelFormat.ASTC_6x6_sRGB);
+ Add(Format.Astc8x5Srgb, MTLPixelFormat.ASTC_8x5_sRGB);
+ Add(Format.Astc8x6Srgb, MTLPixelFormat.ASTC_8x6_sRGB);
+ Add(Format.Astc8x8Srgb, MTLPixelFormat.ASTC_8x8_sRGB);
+ Add(Format.Astc10x5Srgb, MTLPixelFormat.ASTC_10x5_sRGB);
+ Add(Format.Astc10x6Srgb, MTLPixelFormat.ASTC_10x6_sRGB);
+ Add(Format.Astc10x8Srgb, MTLPixelFormat.ASTC_10x8_sRGB);
+ Add(Format.Astc10x10Srgb, MTLPixelFormat.ASTC_10x10_sRGB);
+ Add(Format.Astc12x10Srgb, MTLPixelFormat.ASTC_12x10_sRGB);
+ Add(Format.Astc12x12Srgb, MTLPixelFormat.ASTC_12x12_sRGB);
+ Add(Format.B5G6R5Unorm, MTLPixelFormat.B5G6R5Unorm);
+ Add(Format.B5G5R5A1Unorm, MTLPixelFormat.BGR5A1Unorm);
+ Add(Format.A1B5G5R5Unorm, MTLPixelFormat.A1BGR5Unorm);
+ Add(Format.B8G8R8A8Unorm, MTLPixelFormat.BGRA8Unorm);
+ Add(Format.B8G8R8A8Srgb, MTLPixelFormat.BGRA8Unorm_sRGB);
+ }
+
+ private static void Add(Format format, MTLPixelFormat mtlFormat)
+ {
+ _table[(int)format] = mtlFormat;
+ }
+
+ public static MTLPixelFormat GetFormat(Format format)
+ {
+ return _table[(int)format];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/HardwareInfo.cs b/src/Ryujinx.Graphics.Metal/HardwareInfo.cs
new file mode 100644
index 0000000000..3ca7cdfcab
--- /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 "";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs
new file mode 100644
index 0000000000..0d8fd1f92b
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs
@@ -0,0 +1,246 @@
+using Ryujinx.Common.Configuration;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader.Translation;
+using System;
+using SharpMetal;
+using System.Runtime.CompilerServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ public sealed class MetalRenderer : IRenderer
+ {
+ private MTLDevice _device;
+
+ private Window _window;
+
+ private Pipeline _pipeline;
+
+ internal MTLCommandQueue Queue { get; private set; }
+
+ internal int BufferCount { get; private set; }
+
+ public event EventHandler ScreenCaptured;
+ public bool PreferThreading => true;
+ public IPipeline Pipeline => _pipeline;
+ public IWindow Window => _window;
+
+ public void Initialize(GraphicsDebugLevel logLevel)
+ {
+ _device = MTLDevice.MTLCreateSystemDefaultDevice();
+ Queue = _device.NewCommandQueueWithMaxCommandBufferCount(Constants.MaxCommandBuffersPerQueue);
+
+ var commandBuffer = Queue.CommandBufferWithDescriptor(new MTLCommandBufferDescriptor { RetainedReferences = true });
+
+ _pipeline = new Pipeline(_device, commandBuffer);
+ }
+
+ public void BackgroundContextAction(Action action, bool alwaysBackground = false)
+ {
+ throw new NotImplementedException();
+ }
+
+ public BufferHandle CreateBuffer(int size, BufferHandle storageHint)
+ {
+ return CreateBuffer(size, BufferAccess.Default);
+ }
+
+ public BufferHandle CreateBuffer(IntPtr pointer, int size)
+ {
+ BufferCount++;
+
+ var buffer = _device.NewBufferWithBytesLengthOptions(pointer, (ulong)size, MTLResourceOptions.StorageModeShared);
+ var bufferPtr = buffer.NativePtr;
+ return Unsafe.As(ref bufferPtr);
+ }
+
+ public BufferHandle CreateBuffer(int size, BufferAccess access)
+ {
+ BufferCount++;
+
+ var buffer = _device.NewBufferWithLengthOptions((ulong)size, MTLResourceOptions.StorageModeShared);
+ var bufferPtr = buffer.NativePtr;
+ return Unsafe.As(ref bufferPtr);
+ }
+
+ public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
+ {
+ var library = _device.NewDefaultLibrary();
+ throw new NotImplementedException();
+ }
+
+ public ISampler CreateSampler(SamplerCreateInfo info)
+ {
+ (MTLSamplerMinMagFilter minFilter, MTLSamplerMipFilter mipFilter) = info.MinFilter.Convert();
+
+ var sampler = _device.CreateSamplerState(new MTLSamplerDescriptor
+ {
+ BorderColor = MTLSamplerBorderColor.TransparentBlack,
+ MinFilter = minFilter,
+ MagFilter = info.MagFilter.Convert(),
+ MipFilter = mipFilter,
+ CompareFunction = info.CompareOp.Convert(),
+ LodMinClamp = info.MinLod,
+ LodMaxClamp = info.MaxLod,
+ LodAverage = false,
+ MaxAnisotropy = (uint)info.MaxAnisotropy,
+ SAddressMode = info.AddressU.Convert(),
+ TAddressMode = info.AddressV.Convert(),
+ RAddressMode = info.AddressP.Convert()
+ });
+
+ throw new NotImplementedException();
+ }
+
+ public ITexture CreateTexture(TextureCreateInfo info, float scale)
+ {
+ MTLTextureDescriptor descriptor = new()
+ {
+ PixelFormat = FormatCapabilities.ConvertToMTLFormat(info.Format),
+ TextureType = info.Target.Convert(),
+ Width = (ulong)info.Width,
+ Height = (ulong)info.Height,
+ MipmapLevelCount = (ulong)info.Levels,
+ SampleCount = (ulong)info.Samples,
+ };
+
+ return CreateTextureView(info, scale);
+ }
+
+ internal TextureView CreateTextureView(TextureCreateInfo info, float scale)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool PrepareHostMapping(IntPtr address, ulong size)
+ {
+ // TODO: Metal Host Mapping
+ return false;
+ }
+
+ public void CreateSync(ulong id, bool strict)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DeleteBuffer(BufferHandle buffer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Capabilities GetCapabilities()
+ {
+ // TODO: Finalize these values
+ return new Capabilities(
+ api: TargetApi.Metal,
+ vendorName: HardwareInfoTools.GetVendor(),
+ hasFrontFacingBug: false,
+ hasVectorIndexingBug: true,
+ needsFragmentOutputSpecialization: true,
+ reduceShaderPrecision: true,
+ supportsAstcCompression: true,
+ supportsBc123Compression: true,
+ supportsBc45Compression: true,
+ supportsBc67Compression: true,
+ supportsEtc2Compression: true,
+ supports3DTextureCompression: true,
+ supportsBgraFormat: true,
+ supportsR4G4Format: false,
+ supportsR4G4B4A4Format: true,
+ supportsSnormBufferTextureFormat: true,
+ supports5BitComponentFormat: true,
+ supportsBlendEquationAdvanced: false,
+ supportsFragmentShaderInterlock: true,
+ supportsFragmentShaderOrderingIntel: false,
+ supportsGeometryShader: false,
+ supportsGeometryShaderPassthrough: false,
+ supportsImageLoadFormatted: false,
+ supportsLayerVertexTessellation: false,
+ supportsMismatchingViewFormat: true,
+ supportsCubemapView: true,
+ supportsNonConstantTextureOffset: false,
+ supportsShaderBallot: false,
+ supportsTextureShadowLod: false,
+ supportsViewportIndexVertexTessellation: false,
+ supportsViewportMask: false,
+ supportsViewportSwizzle: false,
+ supportsIndirectParameters: true,
+ maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
+ maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage,
+ maximumTexturesPerStage: Constants.MaxTexturesPerStage,
+ maximumImagesPerStage: Constants.MaxTextureBindings,
+ maximumComputeSharedMemorySize: (int)_device.MaxThreadgroupMemoryLength,
+ maximumSupportedAnisotropy: 0,
+ storageBufferOffsetAlignment: 0,
+ gatherBiasPrecision: 0
+ );
+ }
+
+ public ulong GetCurrentSync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public HardwareInfo GetHardwareInfo()
+ {
+ return new HardwareInfo(HardwareInfoTools.GetVendor(), HardwareInfoTools.GetModel());
+ }
+
+ public IProgram LoadProgramBinary(byte[] programBinary, bool hasFragmentShader, ShaderInfo info)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void UpdateCounters()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void PreFrame()
+ {
+ throw new NotImplementedException();
+ }
+
+ public ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler, bool hostReserved)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ResetCounter(CounterType type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void WaitSync(ulong id)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetInterruptAction(Action interruptAction)
+ {
+ // Not needed for now
+ }
+
+ public void Screenshot()
+ {
+ // TODO: Screenshots
+ }
+
+ public void Dispose()
+ {
+ _window.Dispose();
+ _pipeline.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/Pipeline.cs b/src/Ryujinx.Graphics.Metal/Pipeline.cs
new file mode 100644
index 0000000000..c703a01a1b
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Pipeline.cs
@@ -0,0 +1,314 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using System;
+using SharpMetal;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Graphics.Metal
+{
+ [SupportedOSPlatform("macos")]
+ public class Pipeline : IPipeline, IDisposable
+ {
+ private MTLCommandBuffer _commandBuffer;
+ private MTLRenderCommandEncoder _renderCommandEncoder;
+
+ public Pipeline(MTLDevice device, MTLCommandBuffer commandBuffer)
+ {
+ var renderPipelineDescriptor = new MTLRenderPipelineDescriptor();
+ var renderPipelineState = device.CreateRenderPipelineState(renderPipelineDescriptor, out NSError _);
+
+ _commandBuffer = commandBuffer;
+ _renderCommandEncoder = _commandBuffer.CreateRenderCommandEncoder(new MTLRenderPassDescriptor());
+ _renderCommandEncoder.SetRenderPipelineState(renderPipelineState);
+ }
+
+ public void Barrier()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ClearBuffer(BufferHandle destination, int offset, int size, uint value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ClearRenderTargetColor(int index, int layer, int layerCount, uint componentMask, ColorF color)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue,
+ int stencilMask)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CommandBufferBarrier()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CopyBuffer(BufferHandle source, BufferHandle destination, int srcOffset, int dstOffset, int size)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DispatchCompute(int groupsX, int groupsY, int groupsZ)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawIndexedIndirect(BufferRange indirectBuffer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawIndexedIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawIndirect(BufferRange indirectBuffer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetAlphaTest(bool enable, float reference, CompareOp op)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetBlendState(AdvancedBlendDescriptor blend)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetBlendState(int index, BlendDescriptor blend)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetDepthClamp(bool clamp)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetDepthMode(DepthMode mode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetDepthTest(DepthTestDescriptor depthTest)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetFaceCulling(bool enable, Face face)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetFrontFace(FrontFace frontFace)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetIndexBuffer(BufferRange buffer, IndexType type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetImage(int binding, ITexture texture, Format imageFormat)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetLineParameters(float width, bool smooth)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetLogicOpState(bool enable, LogicalOp op)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetMultisampleState(MultisampleDescriptor multisample)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetPatchParameters(int vertices, ReadOnlySpan defaultOuterLevel, ReadOnlySpan defaultInnerLevel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetPrimitiveRestart(bool enable, int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetPrimitiveTopology(PrimitiveTopology topology)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetProgram(IProgram program)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetRasterizerDiscard(bool discard)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetRenderTargetScale(float scale)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetRenderTargetColorMasks(ReadOnlySpan componentMask)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetScissors(ReadOnlySpan> regions)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetStencilTest(StencilTestDescriptor stencilTest)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetStorageBuffers(ReadOnlySpan buffers)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetUniformBuffers(ReadOnlySpan buffers)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetUserClipDistance(int index, bool enableClip)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetVertexAttribs(ReadOnlySpan vertexAttribs)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetVertexBuffers(ReadOnlySpan vertexBuffers)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetViewports(ReadOnlySpan viewports, bool disableTransform)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void TextureBarrier()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void TextureBarrierTiled()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void EndHostConditionalRendering()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void UpdateRenderScale(ReadOnlySpan scales, int totalCount, int fragmentCount)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void BeginTransformFeedback(PrimitiveTopology topology)
+ {
+ // Metal does not support Transform Feedback
+ throw new NotSupportedException();
+ }
+
+ public void EndTransformFeedback()
+ {
+ // Metal does not support Transform Feedback
+ throw new NotSupportedException();
+ }
+
+ public void SetTransformFeedbackBuffers(ReadOnlySpan buffers)
+ {
+ // Metal does not support Transform Feedback
+ throw new NotSupportedException();
+ }
+
+ public void Dispose()
+ {
+
+ }
+ }
+}
\ No newline at end of file
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 0000000000..6e8b001838
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net7.0
+ true
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/TextureView.cs b/src/Ryujinx.Graphics.Metal/TextureView.cs
new file mode 100644
index 0000000000..0109a4f7e4
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/TextureView.cs
@@ -0,0 +1,77 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Graphics.GAL;
+using System;
+
+namespace Ryujinx.Graphics.Metal
+{
+ class TextureView : ITexture, IDisposable
+ {
+ public int Width { get; }
+ public int Height { get; }
+ public float ScaleFactor { get; }
+ public void CopyTo(ITexture destination, int firstLayer, int firstLevel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CopyTo(BufferRange range, int layer, int level, int stride)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public PinnedSpan GetData()
+ {
+ throw new NotImplementedException();
+ }
+
+ public PinnedSpan GetData(int layer, int level)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetData(SpanOrArray data)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetData(SpanOrArray data, int layer, int level)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetData(SpanOrArray data, int layer, int level, Rectangle region)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetStorage(BufferRange buffer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Release()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Metal/Window.cs b/src/Ryujinx.Graphics.Metal/Window.cs
new file mode 100644
index 0000000000..59122a4939
--- /dev/null
+++ b/src/Ryujinx.Graphics.Metal/Window.cs
@@ -0,0 +1,53 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using SharpMetal;
+
+namespace Ryujinx.Graphics.Metal
+{
+ public class Window : IWindow, IDisposable
+ {
+
+ public Window()
+ {
+ /*var viewport = new MTLViewport
+ {
+
+ };*/
+ }
+
+ public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetSize(int width, int height)
+ {
+ // Not needed as we can get the size from the surface.
+ }
+
+ public void ChangeVSyncMode(bool vsyncEnabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetAntiAliasing(AntiAliasing antialiasing)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetScalingFilter(ScalingFilter type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetScalingFilterLevel(float level)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs
new file mode 100644
index 0000000000..a84d99a04b
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs
@@ -0,0 +1,88 @@
+using Ryujinx.Graphics.Shader.StructuredIr;
+using Ryujinx.Graphics.Shader.Translation;
+using System.Text;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Msl
+{
+ class CodeGenContext
+ {
+ public const string Tab = " ";
+
+ public StructuredProgramInfo Info { get; }
+ public ShaderConfig Config { get; }
+
+ private readonly StringBuilder _sb;
+
+ private int _level;
+
+ private string _indentation;
+
+ public CodeGenContext(StructuredProgramInfo info, ShaderConfig config)
+ {
+ Info = info;
+ Config = Config;
+
+ _sb = new StringBuilder();
+ }
+
+ public void AppendLine()
+ {
+ _sb.AppendLine();
+ }
+
+ public void AppendLine(string str)
+ {
+ _sb.AppendLine(_indentation + str);
+ }
+
+ public string GetCode()
+ {
+ return _sb.ToString();
+ }
+
+ public void EnterScope()
+ {
+ AppendLine("{");
+
+ _level++;
+
+ UpdateIndentation();
+ }
+
+ public void LeaveScope(string suffix = "")
+ {
+ if (_level == 0)
+ {
+ return;
+ }
+
+ _level--;
+
+ UpdateIndentation();
+
+ AppendLine("}" + suffix);
+ }
+
+ public StructuredFunction GetFunction(int id)
+ {
+ return Info.Functions[id];
+ }
+
+ private void UpdateIndentation()
+ {
+ _indentation = GetIndentation(_level);
+ }
+
+ private static string GetIndentation(int level)
+ {
+ string indentation = string.Empty;
+
+ for (int index = 0; index < level; index++)
+ {
+ indentation += Tab;
+ }
+
+ return indentation;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs
new file mode 100644
index 0000000000..47a8b477f2
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Graphics.Shader.CodeGen.Glsl;
+using Ryujinx.Graphics.Shader.StructuredIr;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Msl
+{
+ static class Declarations
+ {
+ public static void Declare(CodeGenContext context, StructuredProgramInfo info)
+ {
+ context.AppendLine("#include ");
+ context.AppendLine("#include ");
+ context.AppendLine();
+ context.AppendLine("using namespace metal;");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs
new file mode 100644
index 0000000000..0dc82390f2
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs
@@ -0,0 +1,17 @@
+using Ryujinx.Graphics.Shader.StructuredIr;
+using Ryujinx.Graphics.Shader.Translation;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Msl
+{
+ static class MslGenerator
+ {
+ public static string Generate(StructuredProgramInfo info, ShaderConfig config)
+ {
+ CodeGenContext context = new CodeGenContext(info, config);
+
+ Declarations.Declare(context, info);
+
+ return context.GetCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs b/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs
index 5196009373..66ed3dd458 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs
@@ -4,5 +4,6 @@ namespace Ryujinx.Graphics.Shader.Translation
{
OpenGL,
Vulkan,
+ Metal
}
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs b/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs
index 863c7447bd..9d58cb926f 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs
@@ -4,6 +4,6 @@ namespace Ryujinx.Graphics.Shader.Translation
{
Glsl,
Spirv,
- Arb,
+ Msl
}
}
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index f4bfd1169e..7642308557 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -28,6 +28,7 @@ using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
+using Ryujinx.Graphics.Metal;
using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
@@ -1067,10 +1068,12 @@ namespace Ryujinx.Ava
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
+ ConfigurationState.Instance.Graphics.GraphicsBackend.Value.ToText(),
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
- $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %"));
+ $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
+ $"GPU: {_renderer.GetHardwareInfo().GpuDriver}"));
}
public async Task ShowExitPrompt()
diff --git a/src/Ryujinx/AppHost.cs.orig b/src/Ryujinx/AppHost.cs.orig
new file mode 100644
index 0000000000..99663fbc54
--- /dev/null
+++ b/src/Ryujinx/AppHost.cs.orig
@@ -0,0 +1,1225 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Input;
+using Avalonia.Threading;
+using LibHac.Tools.FsSystem;
+using Ryujinx.Audio.Backends.Dummy;
+using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Audio.Backends.SoundIo;
+using Ryujinx.Audio.Integration;
+using Ryujinx.Ava.Common;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Input;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.Renderer;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ava.UI.Windows;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Multiplayer;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.SystemInterop;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Gpu;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Graphics.Vulkan;
+using Ryujinx.Graphics.Metal;
+using Ryujinx.HLE;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.HOS.SystemState;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common;
+using Ryujinx.UI.Common.Configuration;
+using Ryujinx.UI.Common.Helper;
+using Silk.NET.Vulkan;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using SPB.Graphics.Vulkan;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
+using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
+using Image = SixLabors.ImageSharp.Image;
+using InputManager = Ryujinx.Input.HLE.InputManager;
+using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
+using Key = Ryujinx.Input.Key;
+using MouseButton = Ryujinx.Input.MouseButton;
+using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
+using Size = Avalonia.Size;
+using Switch = Ryujinx.HLE.Switch;
+
+namespace Ryujinx.Ava
+{
+ internal class AppHost
+ {
+ private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
+ private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
+ private const int TargetFps = 60;
+ private const float VolumeDelta = 0.05f;
+
+ private static readonly Cursor _invisibleCursor = new(StandardCursorType.None);
+ private readonly IntPtr _invisibleCursorWin;
+ private readonly IntPtr _defaultCursorWin;
+
+ private readonly long _ticksPerFrame;
+ private readonly Stopwatch _chrono;
+ private long _ticks;
+
+ private readonly AccountManager _accountManager;
+ private readonly UserChannelPersistence _userChannelPersistence;
+ private readonly InputManager _inputManager;
+
+ private readonly MainWindowViewModel _viewModel;
+ private readonly IKeyboard _keyboardInterface;
+ private readonly TopLevel _topLevel;
+ public RendererHost RendererHost;
+
+ private readonly GraphicsDebugLevel _glLogLevel;
+ private float _newVolume;
+ private KeyboardHotkeyState _prevHotkeyState;
+
+ private long _lastCursorMoveTime;
+ private bool _isCursorInRenderer = true;
+
+ private bool _isStopped;
+ private bool _isActive;
+ private bool _renderingStarted;
+
+ private readonly ManualResetEvent _gpuDoneEvent;
+
+ private IRenderer _renderer;
+ private readonly Thread _renderingThread;
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+ private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
+
+ private bool _dialogShown;
+ private readonly bool _isFirmwareTitle;
+
+ private readonly object _lockObject = new();
+
+ public event EventHandler AppExit;
+ public event EventHandler StatusInitEvent;
+ public event EventHandler StatusUpdatedEvent;
+
+ public VirtualFileSystem VirtualFileSystem { get; }
+ public ContentManager ContentManager { get; }
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+ public Switch Device { get; set; }
+
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+ public string ApplicationPath { get; private set; }
+ public bool ScreenshotRequested { get; set; }
+
+ public AppHost(
+ RendererHost renderer,
+ InputManager inputManager,
+ string applicationPath,
+ VirtualFileSystem virtualFileSystem,
+ ContentManager contentManager,
+ AccountManager accountManager,
+ UserChannelPersistence userChannelPersistence,
+ MainWindowViewModel viewmodel,
+ TopLevel topLevel)
+ {
+ _viewModel = viewmodel;
+ _inputManager = inputManager;
+ _accountManager = accountManager;
+ _userChannelPersistence = userChannelPersistence;
+ _renderingThread = new Thread(RenderLoop) { Name = "GUI.RenderThread" };
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ _glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
+ _topLevel = topLevel;
+
+ _inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
+
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ ApplicationPath = applicationPath;
+ VirtualFileSystem = virtualFileSystem;
+ ContentManager = contentManager;
+
+ RendererHost = renderer;
+
+ _chrono = new Stopwatch();
+ _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+
+ if (ApplicationPath.StartsWith("@SystemContent"))
+ {
+ ApplicationPath = VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
+
+ _isFirmwareTitle = true;
+ }
+
+ ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed;
+
+ _topLevel.PointerMoved += TopLevel_PointerEnteredOrMoved;
+ _topLevel.PointerEntered += TopLevel_PointerEnteredOrMoved;
+ _topLevel.PointerExited += TopLevel_PointerExited;
+
+ if (OperatingSystem.IsWindows())
+ {
+ _invisibleCursorWin = CreateEmptyCursor();
+ _defaultCursorWin = CreateArrowCursor();
+ }
+
+ ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
+ ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
+ ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
+ ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAntiAliasing;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
+ ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
+
+ ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
+ ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
+ ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
+
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+ _gpuDoneEvent = new ManualResetEvent(false);
+ }
+
+ private void TopLevel_PointerEnteredOrMoved(object sender, PointerEventArgs e)
+ {
+ if (sender is MainWindow window)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+
+ var point = e.GetCurrentPoint(window).Position;
+ var bounds = RendererHost.EmbeddedWindow.Bounds;
+
+ _isCursorInRenderer = point.X >= bounds.X &&
+ point.X <= bounds.Width + bounds.X &&
+ point.Y >= bounds.Y &&
+ point.Y <= bounds.Height + bounds.Y;
+ }
+ }
+
+ private void TopLevel_PointerExited(object sender, PointerEventArgs e)
+ {
+ _isCursorInRenderer = false;
+ }
+
+ private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs e)
+ {
+ _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ private void UpdateScalingFilter(object sender, ReactiveEventArgs e)
+ {
+ _renderer.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ _renderer.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs e)
+ {
+ _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
+ }
+
+ private void ShowCursor()
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _viewModel.Cursor = Cursor.Default;
+
+ if (OperatingSystem.IsWindows())
+ {
+ SetCursor(_defaultCursorWin);
+ }
+ });
+ }
+
+ private void HideCursor()
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _viewModel.Cursor = _invisibleCursor;
+
+ if (OperatingSystem.IsWindows())
+ {
+ SetCursor(_invisibleCursorWin);
+ }
+ });
+ }
+
+ private void SetRendererWindowSize(Size size)
+ {
+ if (_renderer != null)
+ {
+ double scale = _topLevel.RenderScaling;
+
+ _renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
+ }
+ }
+
+ private void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
+ {
+ if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
+ {
+ Task.Run(() =>
+ {
+ lock (_lockObject)
+ {
+ string applicationName = Device.Processes.ActiveApplication.Name;
+ string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName);
+ DateTime currentTime = DateTime.Now;
+
+ string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
+
+ string directory = AppDataManager.Mode switch
+ {
+ AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
+ _ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"),
+ };
+
+ string path = Path.Combine(directory, filename);
+
+ try
+ {
+ Directory.CreateDirectory(directory);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
+
+ return;
+ }
+
+ Image image = e.IsBgra ? Image.LoadPixelData(e.Data, e.Width, e.Height)
+ : Image.LoadPixelData(e.Data, e.Width, e.Height);
+
+ if (e.FlipX)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Horizontal));
+ }
+
+ if (e.FlipY)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Vertical));
+ }
+
+ image.SaveAsPng(path, new PngEncoder
+ {
+ ColorType = PngColorType.Rgb,
+ });
+
+ image.Dispose();
+
+ Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
+ }
+ });
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
+ }
+ }
+
+ public void Start()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
+ }
+
+ DisplaySleep.Prevent();
+
+ NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ TouchScreenManager.Initialize(Device);
+
+ _viewModel.IsGameRunning = true;
+
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version);
+ });
+
+ _viewModel.SetUiProgressHandlers(Device);
+
+ RendererHost.BoundsChanged += Window_BoundsChanged;
+
+ _isActive = true;
+
+ _renderingThread.Start();
+
+ _viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
+
+ MainLoop();
+
+ Exit();
+ }
+
+ private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs args)
+ {
+ if (Device != null)
+ {
+ Device.Configuration.IgnoreMissingServices = args.NewValue;
+ }
+ }
+
+ private void UpdateAspectRatioState(object sender, ReactiveEventArgs args)
+ {
+ if (Device != null)
+ {
+ Device.Configuration.AspectRatio = args.NewValue;
+ }
+ }
+
+ private void UpdateAntiAliasing(object sender, ReactiveEventArgs e)
+ {
+ _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
+ }
+
+ private void UpdateDockedModeState(object sender, ReactiveEventArgs e)
+ {
+ Device?.System.ChangeDockedModeState(e.NewValue);
+ }
+
+ private void UpdateAudioVolumeState(object sender, ReactiveEventArgs e)
+ {
+ Device?.SetVolume(e.NewValue);
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _viewModel.Volume = e.NewValue;
+ });
+ }
+
+ private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.EnableInternetAccess = e.NewValue;
+ }
+
+ private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
+ }
+
+ private void UpdateMultiplayerModeState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.MultiplayerMode = e.NewValue;
+ }
+
+ public void ToggleVSync()
+ {
+ Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
+ _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
+ }
+
+ public void Stop()
+ {
+ _isActive = false;
+ }
+
+ private void Exit()
+ {
+ (_keyboardInterface as AvaloniaKeyboard)?.Clear();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _isStopped = true;
+ _isActive = false;
+ }
+
+ public void DisposeContext()
+ {
+ Dispose();
+
+ _isActive = false;
+
+ // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
+ // We only need to wait for all commands submitted during the main gpu loop to be processed.
+ _gpuDoneEvent.WaitOne();
+ _gpuDoneEvent.Dispose();
+
+ DisplaySleep.Restore();
+
+ NpadManager.Dispose();
+ TouchScreenManager.Dispose();
+ Device.Dispose();
+
+ DisposeGpu();
+
+ AppExit?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void Dispose()
+ {
+ if (Device.Processes != null)
+ {
+ MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
+ }
+
+ ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
+ ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
+ ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAntiAliasing;
+ ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event -= UpdateColorSpacePassthrough;
+
+ _topLevel.PointerMoved -= TopLevel_PointerEnteredOrMoved;
+ _topLevel.PointerEntered -= TopLevel_PointerEnteredOrMoved;
+ _topLevel.PointerExited -= TopLevel_PointerExited;
+
+ _gpuCancellationTokenSource.Cancel();
+ _gpuCancellationTokenSource.Dispose();
+
+ _chrono.Stop();
+ }
+
+ public void DisposeGpu()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution?.Dispose();
+ _windowsMultimediaTimerResolution = null;
+ }
+
+ if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow)
+ {
+ // Try to bind the OpenGL context before calling the shutdown event.
+ openGlWindow.MakeCurrent(false, false);
+
+ Device.DisposeGpu();
+
+ // Unbind context and destroy everything.
+ openGlWindow.MakeCurrent(true, false);
+ }
+ else
+ {
+ Device.DisposeGpu();
+ }
+ }
+
+ private void HideCursorState_Changed(object sender, ReactiveEventArgs state)
+ {
+ if (state.NewValue == HideCursorMode.OnIdle)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+ }
+
+ public async Task LoadGuestApplication()
+ {
+ InitializeSwitchInstance();
+ MainWindow.UpdateGraphicsConfig();
+
+ SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
+
+ if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
+ {
+ {
+ if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
+ {
+ if (userError == UserError.NoFirmware)
+ {
+ UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
+ LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
+ LocaleManager.Instance[LocaleKeys.InputDialogYes],
+ LocaleManager.Instance[LocaleKeys.InputDialogNo],
+ "");
+
+ if (result != UserResult.Yes)
+ {
+ await UserErrorDialog.ShowUserErrorDialog(userError);
+ Device.Dispose();
+
+ return false;
+ }
+ }
+
+ if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
+ {
+ await UserErrorDialog.ShowUserErrorDialog(userError);
+ Device.Dispose();
+
+ return false;
+ }
+
+ // Tell the user that we installed a firmware for them.
+ if (userError == UserError.NoFirmware)
+ {
+ firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
+
+ _viewModel.RefreshFirmwareStatus();
+
+ await ContentDialogHelper.CreateInfoDialog(
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
+ LocaleManager.Instance[LocaleKeys.InputDialogOk],
+ "",
+ LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
+ }
+ }
+ else
+ {
+ await UserErrorDialog.ShowUserErrorDialog(userError);
+ Device.Dispose();
+
+ return false;
+ }
+ }
+ }
+ }
+
+ Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
+
+ if (_isFirmwareTitle)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
+
+ if (!Device.LoadNca(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+ }
+ else if (Directory.Exists(ApplicationPath))
+ {
+ string[] romFsFiles = Directory.GetFiles(ApplicationPath, "*.istorage");
+
+ if (romFsFiles.Length == 0)
+ {
+ romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
+ }
+
+ if (romFsFiles.Length > 0)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
+
+ if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
+
+ if (!Device.LoadCart(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+ }
+ }
+ else if (File.Exists(ApplicationPath))
+ {
+ switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
+ {
+ case ".xci":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
+
+ if (!Device.LoadXci(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+
+ break;
+ }
+ case ".nca":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
+
+ if (!Device.LoadNca(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+
+ break;
+ }
+ case ".nsp":
+ case ".pfs0":
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
+
+ if (!Device.LoadNsp(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+
+ break;
+ }
+ default:
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as homebrew.");
+
+ try
+ {
+ if (!Device.LoadProgram(ApplicationPath))
+ {
+ Device.Dispose();
+
+ return false;
+ }
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
+
+ Device.Dispose();
+
+ return false;
+ }
+
+ break;
+ }
+ }
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
+
+ Device.Dispose();
+
+ return false;
+ }
+
+ DiscordIntegrationModule.SwitchToPlayingState(Device.Processes.ActiveApplication.ProgramIdText, Device.Processes.ActiveApplication.Name);
+
+ ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
+ {
+ appMetadata.UpdatePreGame();
+ });
+
+ return true;
+ }
+
+ internal void Resume()
+ {
+ Device?.System.TogglePauseEmulation(false);
+
+ _viewModel.IsPaused = false;
+ _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
+ Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
+ }
+
+ internal void Pause()
+ {
+ Device?.System.TogglePauseEmulation(true);
+
+ _viewModel.IsPaused = true;
+ _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
+ Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
+ }
+
+ private void InitializeSwitchInstance()
+ {
+ // Initialize KeySet.
+ VirtualFileSystem.ReloadKeySet();
+
+ // Initialize Renderer.
+ IRenderer renderer;
+
+ if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan)
+ {
+ renderer = new VulkanRenderer(
+ Vk.GetApi(),
+ (RendererHost.EmbeddedWindow as EmbeddedWindowVulkan).CreateSurface,
+ VulkanHelper.GetRequiredInstanceExtensions,
+ ConfigurationState.Instance.Graphics.PreferredGpu.Value);
+ }
+ else if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Metal)
+ {
+ renderer = new MetalRenderer();
+ }
+ else
+ {
+ renderer = new OpenGLRenderer();
+ }
+
+ BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
+
+ var isGALThreaded = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
+ if (isGALThreaded)
+ {
+ renderer = new ThreadedRenderer(renderer);
+ }
+
+ Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {isGALThreaded}");
+
+ // Initialize Configuration.
+ var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB;
+
+ HLEConfiguration configuration = new(VirtualFileSystem,
+ _viewModel.LibHacHorizonManager,
+ ContentManager,
+ _accountManager,
+ _userChannelPersistence,
+ renderer,
+ InitializeAudio(),
+ memoryConfiguration,
+ _viewModel.UiHandler,
+ (SystemLanguage)ConfigurationState.Instance.System.Language.Value,
+ (RegionCode)ConfigurationState.Instance.System.Region.Value,
+ ConfigurationState.Instance.Graphics.EnableVsync,
+ ConfigurationState.Instance.System.EnableDockedMode,
+ ConfigurationState.Instance.System.EnablePtc,
+ ConfigurationState.Instance.System.EnableInternetAccess,
+ ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
+ ConfigurationState.Instance.System.FsGlobalAccessLogMode,
+ ConfigurationState.Instance.System.SystemTimeOffset,
+ ConfigurationState.Instance.System.TimeZone,
+ ConfigurationState.Instance.System.MemoryManagerMode,
+ ConfigurationState.Instance.System.IgnoreMissingServices,
+ ConfigurationState.Instance.Graphics.AspectRatio,
+ ConfigurationState.Instance.System.AudioVolume,
+ ConfigurationState.Instance.System.UseHypervisor,
+ ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
+ ConfigurationState.Instance.Multiplayer.Mode);
+
+ Device = new Switch(configuration);
+ }
+
+ private static IHardwareDeviceDriver InitializeAudio()
+ {
+ var availableBackends = new List
+ {
+ AudioBackend.SDL2,
+ AudioBackend.SoundIo,
+ AudioBackend.OpenAl,
+ AudioBackend.Dummy,
+ };
+
+ AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
+
+ for (int i = 0; i < availableBackends.Count; i++)
+ {
+ if (availableBackends[i] == preferredBackend)
+ {
+ availableBackends.RemoveAt(i);
+ availableBackends.Insert(0, preferredBackend);
+ break;
+ }
+ }
+
+ static IHardwareDeviceDriver InitializeAudioBackend(AudioBackend backend, AudioBackend nextBackend) where T : IHardwareDeviceDriver, new()
+ {
+ if (T.IsSupported)
+ {
+ return new T();
+ }
+
+ Logger.Warning?.Print(LogClass.Audio, $"{backend} is not supported, falling back to {nextBackend}.");
+
+ return null;
+ }
+
+ IHardwareDeviceDriver deviceDriver = null;
+
+ for (int i = 0; i < availableBackends.Count; i++)
+ {
+ AudioBackend currentBackend = availableBackends[i];
+ AudioBackend nextBackend = i + 1 < availableBackends.Count ? availableBackends[i + 1] : AudioBackend.Dummy;
+
+ deviceDriver = currentBackend switch
+ {
+ AudioBackend.SDL2 => InitializeAudioBackend(AudioBackend.SDL2, nextBackend),
+ AudioBackend.SoundIo => InitializeAudioBackend(AudioBackend.SoundIo, nextBackend),
+ AudioBackend.OpenAl => InitializeAudioBackend(AudioBackend.OpenAl, nextBackend),
+ _ => new DummyHardwareDeviceDriver(),
+ };
+
+ if (deviceDriver != null)
+ {
+ ConfigurationState.Instance.System.AudioBackend.Value = currentBackend;
+ break;
+ }
+ }
+
+ MainWindowViewModel.SaveConfig();
+
+ return deviceDriver;
+ }
+
+ private void Window_BoundsChanged(object sender, Size e)
+ {
+ Width = (int)e.Width;
+ Height = (int)e.Height;
+
+ SetRendererWindowSize(e);
+ }
+
+ private void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ // Polling becomes expensive if it's not slept.
+ Thread.Sleep(1);
+ }
+ }
+
+ private void RenderLoop()
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (_viewModel.StartGamesInFullscreen)
+ {
+ _viewModel.WindowState = WindowState.FullScreen;
+ }
+
+ if (_viewModel.WindowState == WindowState.FullScreen)
+ {
+ _viewModel.ShowMenuAndStatusBar = false;
+ }
+ });
+
+ _renderer = Device.Gpu.Renderer is ThreadedRenderer tr ? tr.BaseRenderer : Device.Gpu.Renderer;
+
+ _renderer.ScreenCaptured += Renderer_ScreenCaptured;
+
+ (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.InitializeBackgroundContext(_renderer);
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ _renderer?.Window?.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
+ _renderer?.Window?.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ _renderer?.Window?.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ _renderer?.Window?.SetColorSpacePassthrough(ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
+
+ Width = (int)RendererHost.Bounds.Width;
+ Height = (int)RendererHost.Bounds.Height;
+
+ _renderer.Window.SetSize((int)(Width * _topLevel.RenderScaling), (int)(Height * _topLevel.RenderScaling));
+
+ _chrono.Start();
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+
+ _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
+
+ while (_isActive)
+ {
+ _ticks += _chrono.ElapsedTicks;
+
+ _chrono.Restart();
+
+ if (Device.WaitFifo())
+ {
+ Device.Statistics.RecordFifoStart();
+ Device.ProcessFrame();
+ Device.Statistics.RecordFifoEnd();
+ }
+
+ while (Device.ConsumeFrameAvailable())
+ {
+ if (!_renderingStarted)
+ {
+ _renderingStarted = true;
+ _viewModel.SwitchToRenderer(false);
+ InitStatus();
+ }
+
+ Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
+ }
+
+ if (_ticks >= _ticksPerFrame)
+ {
+ UpdateStatus();
+ }
+ }
+
+ // Make sure all commands in the run loop are fully executed before leaving the loop.
+ if (Device.Gpu.Renderer is ThreadedRenderer threaded)
+ {
+ threaded.FlushThreadedCommands();
+ }
+
+ _gpuDoneEvent.Set();
+ });
+
+ (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true);
+ }
+
+ public void InitStatus()
+ {
+ StatusInitEvent?.Invoke(this, new StatusInitEventArgs(
+ ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch
+ {
+ GraphicsBackend.Vulkan => "Vulkan",
+ GraphicsBackend.OpenGl => "OpenGL",
+ _ => throw new NotImplementedException()
+ },
+ $"GPU: {_renderer.GetHardwareInfo().GpuDriver}"));
+ }
+
+ public void UpdateStatus()
+ {
+ // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued.
+ string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
+
+ if (GraphicsConfig.ResScale != 1)
+ {
+ dockedMode += $" ({GraphicsConfig.ResScale}x)";
+ }
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
+<<<<<<< HEAD
+=======
+ ConfigurationState.Instance.Graphics.GraphicsBackend.Value.ToText(),
+>>>>>>> 2daba02b8 (Start Metal Backend)
+ dockedMode,
+ ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
+ LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %"));
+ }
+
+ public async Task ShowExitPrompt()
+ {
+ bool shouldExit = !ConfigurationState.Instance.ShowConfirmExit;
+ if (!shouldExit)
+ {
+ if (_dialogShown)
+ {
+ return;
+ }
+
+ _dialogShown = true;
+
+ shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
+
+ _dialogShown = false;
+ }
+
+ if (shouldExit)
+ {
+ Stop();
+ }
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return false;
+ }
+
+ NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+
+ if (_viewModel.IsActive)
+ {
+ if (_isCursorInRenderer)
+ {
+ if (ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ HideCursor();
+ }
+ else
+ {
+ switch (ConfigurationState.Instance.HideCursor.Value)
+ {
+ case HideCursorMode.Never:
+ ShowCursor();
+ break;
+ case HideCursorMode.OnIdle:
+ if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
+ {
+ HideCursor();
+ }
+ else
+ {
+ ShowCursor();
+ }
+ break;
+ case HideCursorMode.Always:
+ HideCursor();
+ break;
+ }
+ }
+ }
+ else
+ {
+ ShowCursor();
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
+ {
+ Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
+ }
+ });
+
+ KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
+
+ if (currentHotkeyState != _prevHotkeyState)
+ {
+ switch (currentHotkeyState)
+ {
+ case KeyboardHotkeyState.ToggleVSync:
+ ToggleVSync();
+ break;
+ case KeyboardHotkeyState.Screenshot:
+ ScreenshotRequested = true;
+ break;
+ case KeyboardHotkeyState.ShowUI:
+ _viewModel.ShowMenuAndStatusBar = !_viewModel.ShowMenuAndStatusBar;
+ break;
+ case KeyboardHotkeyState.Pause:
+ if (_viewModel.IsPaused)
+ {
+ Resume();
+ }
+ else
+ {
+ Pause();
+ }
+ break;
+ case KeyboardHotkeyState.ToggleMute:
+ if (Device.IsAudioMuted())
+ {
+ Device.SetVolume(_viewModel.VolumeBeforeMute);
+ }
+ else
+ {
+ _viewModel.VolumeBeforeMute = Device.GetVolume();
+ Device.SetVolume(0);
+ }
+
+ _viewModel.Volume = Device.GetVolume();
+ break;
+ case KeyboardHotkeyState.ResScaleUp:
+ GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
+ break;
+ case KeyboardHotkeyState.ResScaleDown:
+ GraphicsConfig.ResScale =
+ (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
+ break;
+ case KeyboardHotkeyState.VolumeUp:
+ _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+
+ _viewModel.Volume = Device.GetVolume();
+ break;
+ case KeyboardHotkeyState.VolumeDown:
+ _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+
+ _viewModel.Volume = Device.GetVolume();
+ break;
+ case KeyboardHotkeyState.None:
+ (_keyboardInterface as AvaloniaKeyboard).Clear();
+ break;
+ }
+ }
+
+ _prevHotkeyState = currentHotkeyState;
+
+ if (ScreenshotRequested)
+ {
+ ScreenshotRequested = false;
+ _renderer.Screenshot();
+ }
+ }
+
+ // Touchscreen.
+ bool hasTouch = false;
+
+ if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ Device.Hid.Touchscreen.Update();
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ return true;
+ }
+
+ private KeyboardHotkeyState GetHotkeyState()
+ {
+ KeyboardHotkeyState state = KeyboardHotkeyState.None;
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
+ {
+ state = KeyboardHotkeyState.ToggleVSync;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
+ {
+ state = KeyboardHotkeyState.Screenshot;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI))
+ {
+ state = KeyboardHotkeyState.ShowUI;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
+ {
+ state = KeyboardHotkeyState.Pause;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
+ {
+ state = KeyboardHotkeyState.ToggleMute;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
+ {
+ state = KeyboardHotkeyState.ResScaleUp;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
+ {
+ state = KeyboardHotkeyState.ResScaleDown;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
+ {
+ state = KeyboardHotkeyState.VolumeUp;
+ }
+ else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
+ {
+ state = KeyboardHotkeyState.VolumeDown;
+ }
+
+ return state;
+ }
+ }
+}
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 6c83cedcf3..a5890dc08e 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -188,6 +188,10 @@ namespace Ryujinx.Ava
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
}
+ else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "metal")
+ {
+ ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Metal;
+ }
}
// Check if docked mode was overriden.
diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs
new file mode 100644
index 0000000000..a8bac75c0d
--- /dev/null
+++ b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs
@@ -0,0 +1,25 @@
+using SPB.Windowing;
+using SPB.Platform.Metal;
+using System;
+
+namespace Ryujinx.UI.Renderer
+{
+ public class EmbeddedWindowMetal : EmbeddedWindow
+ {
+ public SimpleMetalWindow CreateSurface()
+ {
+ SimpleMetalWindow simpleMetalWindow;
+
+ if (OperatingSystem.IsMacOS())
+ {
+ simpleMetalWindow = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ return simpleMetalWindow;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 70e5fa5c74..75c5072476 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -111,6 +111,8 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
+ public bool IsMetalAvailable => OperatingSystem.IsMacOS();
+
public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS();
public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
index 0a12575adc..da9cdee2e0 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
@@ -43,6 +43,9 @@
+
+
+