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/EncoderState.cs b/src/Ryujinx.Graphics.Metal/EncoderState.cs index 0bd2e9651..54f30e985 100644 --- a/src/Ryujinx.Graphics.Metal/EncoderState.cs +++ b/src/Ryujinx.Graphics.Metal/EncoderState.cs @@ -73,6 +73,9 @@ namespace Ryujinx.Graphics.Metal // Dirty flags public DirtyFlags Dirty = new(); + // Only to be used for present + public bool ClearLoadAction = false; + public EncoderState() { } public EncoderState Clone() diff --git a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs index 849461802..d3d23c12c 100644 --- a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs +++ b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs @@ -24,8 +24,6 @@ namespace Ryujinx.Graphics.Metal public readonly MTLIndexType IndexType => _currentState.IndexType; public readonly ulong IndexBufferOffset => _currentState.IndexBufferOffset; public readonly PrimitiveTopology Topology => _currentState.Topology; - public readonly Texture[] RenderTargets => _currentState.RenderTargets; - public readonly Texture DepthStencil => _currentState.DepthStencil; public EncoderStateManager(MTLDevice device, Pipeline pipeline) { @@ -82,6 +80,11 @@ namespace Ryujinx.Graphics.Metal } } + public void SetClearLoadAction(bool clear) + { + _currentState.ClearLoadAction = clear; + } + public MTLRenderCommandEncoder CreateRenderCommandEncoder() { // Initialise Pass & State @@ -93,7 +96,7 @@ namespace Ryujinx.Graphics.Metal { var passAttachment = renderPassDescriptor.ColorAttachments.Object((ulong)i); passAttachment.Texture = _currentState.RenderTargets[i].MTLTexture; - passAttachment.LoadAction = MTLLoadAction.Load; + passAttachment.LoadAction = _currentState.ClearLoadAction ? MTLLoadAction.Clear : MTLLoadAction.Load; passAttachment.StoreAction = MTLStoreAction.Store; } } @@ -661,11 +664,18 @@ namespace Ryujinx.Graphics.Metal // TODO: Handle 'zero' buffers for (int i = 0; i < attribDescriptors.Length; i++) { - var attrib = vertexDescriptor.Attributes.Object((ulong)i); - attrib.Format = attribDescriptors[i].Format.Convert(); - indexMask |= 1u << attribDescriptors[i].BufferIndex; - attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex; - attrib.Offset = (ulong)attribDescriptors[i].Offset; + if (!attribDescriptors[i].IsZero) + { + var attrib = vertexDescriptor.Attributes.Object((ulong)i); + attrib.Format = attribDescriptors[i].Format.Convert(); + indexMask |= 1u << attribDescriptors[i].BufferIndex; + attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex; + attrib.Offset = (ulong)attribDescriptors[i].Offset; + } + else + { + // Logger.Warning?.PrintMsg(LogClass.Gpu, "Unhandled IsZero buffer!"); + } } for (int i = 0; i < bufferDescriptors.Length; i++) diff --git a/src/Ryujinx.Graphics.Metal/HelperShader.cs b/src/Ryujinx.Graphics.Metal/HelperShader.cs index c2f7a012f..1074a82ee 100644 --- a/src/Ryujinx.Graphics.Metal/HelperShader.cs +++ b/src/Ryujinx.Graphics.Metal/HelperShader.cs @@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.Metal private readonly Pipeline _pipeline; private MTLDevice _device; + private readonly ISampler _samplerLinear; + private readonly ISampler _samplerNearest; private readonly IProgram _programColorBlit; private readonly List _programsColorClear = new(); private readonly IProgram _programDepthStencilClear; @@ -25,6 +27,9 @@ namespace Ryujinx.Graphics.Metal _device = device; _pipeline = pipeline; + _samplerNearest = new Sampler(_device, SamplerCreateInfo.Create(MinFilter.Nearest, MagFilter.Nearest)); + _samplerLinear = new Sampler(_device, SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); + var blitSource = ReadMsl("Blit.metal"); _programColorBlit = new Program( [ @@ -56,28 +61,140 @@ namespace Ryujinx.Graphics.Metal return EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName)); } - public void BlitColor( - ITexture source, - ITexture destination) + public unsafe void BlitColor( + ITexture src, + ITexture dst, + Extents2D srcRegion, + Extents2D dstRegion, + bool linearFilter) { - var sampler = _device.NewSamplerState(new MTLSamplerDescriptor + const int RegionBufferSize = 16; + + var sampler = linearFilter ? _samplerLinear : _samplerNearest; + + 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) { - MinFilter = MTLSamplerMinMagFilter.Nearest, - MagFilter = MTLSamplerMinMagFilter.Nearest, - MipFilter = MTLSamplerMipFilter.NotMipmapped - }); + (region[0], region[1]) = (region[1], region[0]); + } + + if (dstRegion.Y1 > dstRegion.Y2) + { + (region[2], region[3]) = (region[3], region[2]); + } + + 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[1]; + + viewports[0] = new Viewport( + rect, + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + int dstWidth = dst.Width; + int dstHeight = dst.Height; // Save current state _pipeline.SaveAndResetState(); _pipeline.SetProgram(_programColorBlit); - // Viewport and scissor needs to be set before render pass begin so as not to bind the old ones - _pipeline.SetViewports([]); - _pipeline.SetScissors([]); - _pipeline.SetRenderTargets([destination], null); - _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, source, new Sampler(sampler)); - _pipeline.SetPrimitiveTopology(PrimitiveTopology.Triangles); - _pipeline.Draw(6, 1, 0, 0); + _pipeline.SetViewports(viewports); + _pipeline.SetScissors(stackalloc Rectangle[] { new Rectangle(0, 0, dstWidth, dstHeight) }); + _pipeline.SetRenderTargets([dst], null); + _pipeline.SetClearLoadAction(true); + _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, sampler); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + + fixed (float* ptr = region) + { + _pipeline.GetOrCreateRenderEncoder().SetVertexBytes((IntPtr)ptr, RegionBufferSize, 0); + } + + _pipeline.Draw(4, 1, 0, 0); + + // Restore previous state + _pipeline.RestoreState(); + } + + public unsafe void DrawTexture( + ITexture src, + ISampler srcSampler, + Extents2DF srcRegion, + Extents2DF dstRegion) + { + const int RegionBufferSize = 16; + + 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]); + } + + Span viewports = stackalloc Viewport[1]; + Span> scissors = stackalloc Rectangle[1]; + + 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); + + scissors[0] = new Rectangle(0, 0, 0xFFFF, 0xFFFF); + + // Save current state + _pipeline.SaveState(); + + _pipeline.SetProgram(_programColorBlit); + _pipeline.SetViewports(viewports); + _pipeline.SetScissors(scissors); + _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, srcSampler); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + _pipeline.SetFaceCulling(false, Face.FrontAndBack); + // For some reason this results in a SIGSEGV + // _pipeline.SetStencilTest(CreateStencilTestDescriptor(false)); + _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always)); + + fixed (float* ptr = region) + { + _pipeline.GetOrCreateRenderEncoder().SetVertexBytes((IntPtr)ptr, RegionBufferSize, 0); + } + + _pipeline.Draw(4, 1, 0, 0); // Restore previous state _pipeline.RestoreState(); @@ -169,6 +286,8 @@ namespace Ryujinx.Graphics.Metal } _programDepthStencilClear.Dispose(); _pipeline.Dispose(); + _samplerLinear.Dispose(); + _samplerNearest.Dispose(); } } } diff --git a/src/Ryujinx.Graphics.Metal/Pipeline.cs b/src/Ryujinx.Graphics.Metal/Pipeline.cs index 2734e6930..2d66a36f0 100644 --- a/src/Ryujinx.Graphics.Metal/Pipeline.cs +++ b/src/Ryujinx.Graphics.Metal/Pipeline.cs @@ -61,6 +61,11 @@ namespace Ryujinx.Graphics.Metal _encoderStateManager.RestoreState(); } + public void SetClearLoadAction(bool clear) + { + _encoderStateManager.SetClearLoadAction(clear); + } + public MTLRenderCommandEncoder GetOrCreateRenderEncoder() { MTLRenderCommandEncoder renderCommandEncoder; @@ -167,22 +172,17 @@ namespace Ryujinx.Graphics.Metal return computeCommandEncoder; } - public void Present(CAMetalDrawable drawable, ITexture texture) + public void Present(CAMetalDrawable drawable, Texture src, Extents2D srcRegion, Extents2D dstRegion, bool isLinear) { - if (texture is not Texture tex) - { - return; - } - EndCurrentPass(); SaveState(); // 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 dest = new Texture(_device, this, textureInfo, drawable.Texture, 0, 0); + var dst = new Texture(_device, this, textureInfo, drawable.Texture, 0, 0); - _helperShader.BlitColor(tex, dest); + _helperShader.BlitColor(src, dst, srcRegion, dstRegion, isLinear); EndCurrentPass(); @@ -194,7 +194,7 @@ namespace Ryujinx.Graphics.Metal RestoreState(); // Cleanup - dest.Dispose(); + dst.Dispose(); } public void Barrier() @@ -338,9 +338,7 @@ namespace Ryujinx.Graphics.Metal public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) { - // var renderCommandEncoder = GetOrCreateRenderEncoder(); - - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + _helperShader.DrawTexture(texture, sampler, srcRegion, dstRegion); } public void SetAlphaTest(bool enable, float reference, CompareOp op) diff --git a/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal index b2bec3e8e..3d86a27a8 100644 --- a/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal +++ b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal @@ -2,32 +2,23 @@ using namespace metal; -// ------------------ -// Simple Blit Shader -// ------------------ - -constant float2 quadVertices[] = { - float2(-1, -1), - float2(-1, 1), - float2( 1, 1), - float2(-1, -1), - float2( 1, 1), - float2( 1, -1) -}; - struct CopyVertexOut { float4 position [[position]]; float2 uv; }; -vertex CopyVertexOut vertexMain(unsigned short vid [[vertex_id]]) { - float2 position = quadVertices[vid]; - +vertex CopyVertexOut vertexMain(uint vid [[vertex_id]], + const device float* texCoord [[buffer(0)]]) { CopyVertexOut out; - out.position = float4(position, 0, 1); - out.position.y = -out.position.y; - out.uv = position * 0.5f + 0.5f; + int low = vid & 1; + int high = vid >> 1; + out.uv.x = texCoord[low]; + out.uv.y = texCoord[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; } diff --git a/src/Ryujinx.Graphics.Metal/Window.cs b/src/Ryujinx.Graphics.Metal/Window.cs index 64410df6d..67585f180 100644 --- a/src/Ryujinx.Graphics.Metal/Window.cs +++ b/src/Ryujinx.Graphics.Metal/Window.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Metal.Effects; using SharpMetal.ObjectiveCCore; using SharpMetal.QuartzCore; using System; @@ -10,51 +11,196 @@ namespace Ryujinx.Graphics.Metal [SupportedOSPlatform("macos")] class Window : IWindow, IDisposable { + public bool ScreenCaptureRequested { get; set; } + private readonly MetalRenderer _renderer; private readonly CAMetalLayer _metalLayer; + private int _width; + private int _height; + private bool _vsyncEnabled; + private AntiAliasing _currentAntiAliasing; + private bool _updateEffect; + private IPostProcessingEffect _effect; + private IScalingFilter _scalingFilter; + private bool _isLinear; + private float _scalingFilterLevel; + private bool _updateScalingFilter; + private ScalingFilter _currentScalingFilter; + private bool _colorSpacePassthroughEnabled; + public Window(MetalRenderer renderer, CAMetalLayer metalLayer) { _renderer = renderer; _metalLayer = metalLayer; } - // TODO: Handle ImageCrop public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) { if (_renderer.Pipeline is Pipeline pipeline && texture is Texture tex) { var drawable = new CAMetalDrawable(ObjectiveC.IntPtr_objc_msgSend(_metalLayer, "nextDrawable")); - pipeline.Present(drawable, tex); + + _width = (int)drawable.Texture.Width; + _height = (int)drawable.Texture.Height; + + UpdateEffect(); + + if (_effect != null) + { + // TODO: Run Effects + // view = _effect.Run() + } + + int srcX0, srcX1, srcY0, srcY1; + + if (crop.Left == 0 && crop.Right == 0) + { + srcX0 = 0; + srcX1 = tex.Width; + } + else + { + srcX0 = crop.Left; + srcX1 = crop.Right; + } + + if (crop.Top == 0 && crop.Bottom == 0) + { + srcY0 = 0; + srcY1 = tex.Height; + } + else + { + srcY0 = crop.Top; + srcY1 = crop.Bottom; + } + + if (ScreenCaptureRequested) + { + // TODO: Support screen captures + + ScreenCaptureRequested = false; + } + + float ratioX = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _height * crop.AspectRatioX / (_width * crop.AspectRatioY)); + float ratioY = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _width * crop.AspectRatioY / (_height * crop.AspectRatioX)); + + int dstWidth = (int)(_width * ratioX); + int dstHeight = (int)(_height * ratioY); + + int dstPaddingX = (_width - dstWidth) / 2; + int dstPaddingY = (_height - dstHeight) / 2; + + int dstX0 = crop.FlipX ? _width - dstPaddingX : dstPaddingX; + int dstX1 = crop.FlipX ? dstPaddingX : _width - dstPaddingX; + + int dstY0 = crop.FlipY ? _height - dstPaddingY : dstPaddingY; + int dstY1 = crop.FlipY ? dstPaddingY : _height - dstPaddingY; + + if (_scalingFilter != null) + { + // TODO: Run scaling filter + } + + pipeline.Present( + drawable, + tex, + new Extents2D(srcX0, srcY0, srcX1, srcY1), + new Extents2D(dstX0, dstY0, dstX1, dstY1), + _isLinear); } } public void SetSize(int width, int height) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + // Ignore } public void ChangeVSyncMode(bool vsyncEnabled) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + _vsyncEnabled = vsyncEnabled; } - public void SetAntiAliasing(AntiAliasing antialiasing) + public void SetAntiAliasing(AntiAliasing effect) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + if (_currentAntiAliasing == effect && _effect != null) + { + return; + } + + _currentAntiAliasing = effect; + + _updateEffect = true; } public void SetScalingFilter(ScalingFilter type) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + if (_currentScalingFilter == type && _effect != null) + { + return; + } + + _currentScalingFilter = type; + + _updateScalingFilter = true; } public void SetScalingFilterLevel(float level) { - Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + _scalingFilterLevel = level; + _updateScalingFilter = true; } - public void SetColorSpacePassthrough(bool colorSpacePassThroughEnabled) { } + public void SetColorSpacePassthrough(bool colorSpacePassThroughEnabled) + { + _colorSpacePassthroughEnabled = colorSpacePassThroughEnabled; + } + + private void UpdateEffect() + { + if (_updateEffect) + { + _updateEffect = false; + + switch (_currentAntiAliasing) + { + case AntiAliasing.Fxaa: + _effect?.Dispose(); + Logger.Warning?.PrintMsg(LogClass.Gpu, "FXAA not implemented for Metal backend!"); + break; + case AntiAliasing.None: + _effect?.Dispose(); + _effect = null; + break; + case AntiAliasing.SmaaLow: + case AntiAliasing.SmaaMedium: + case AntiAliasing.SmaaHigh: + case AntiAliasing.SmaaUltra: + var quality = _currentAntiAliasing - AntiAliasing.SmaaLow; + Logger.Warning?.PrintMsg(LogClass.Gpu, "SMAA not implemented for Metal backend!"); + break; + } + } + + if (_updateScalingFilter) + { + _updateScalingFilter = false; + + switch (_currentScalingFilter) + { + case ScalingFilter.Bilinear: + case ScalingFilter.Nearest: + _scalingFilter?.Dispose(); + _scalingFilter = null; + _isLinear = _currentScalingFilter == ScalingFilter.Bilinear; + break; + case ScalingFilter.Fsr: + Logger.Warning?.PrintMsg(LogClass.Gpu, "FSR not implemented for Metal backend!"); + break; + } + } + } public void Dispose() {