Metal: Advanced Present (#6)

* Initial DrawTexture support & Advanced Present

* TODO: Get Scissors Working

* Chnage scissor state management

* Rebase problems…

* Rebase fixes again

* Update DrawTexture + Fix Topology

* Fix flipping

* Add clear action support

* Cleanup
This commit is contained in:
Isaac Marovitz 2024-05-27 09:47:50 -04:00
parent 1f91c74a95
commit 1f29a76ea3
8 changed files with 358 additions and 63 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -73,6 +73,9 @@ namespace Ryujinx.Graphics.Metal
// Dirty flags // Dirty flags
public DirtyFlags Dirty = new(); public DirtyFlags Dirty = new();
// Only to be used for present
public bool ClearLoadAction = false;
public EncoderState() { } public EncoderState() { }
public EncoderState Clone() public EncoderState Clone()

View file

@ -24,8 +24,6 @@ namespace Ryujinx.Graphics.Metal
public readonly MTLIndexType IndexType => _currentState.IndexType; public readonly MTLIndexType IndexType => _currentState.IndexType;
public readonly ulong IndexBufferOffset => _currentState.IndexBufferOffset; public readonly ulong IndexBufferOffset => _currentState.IndexBufferOffset;
public readonly PrimitiveTopology Topology => _currentState.Topology; public readonly PrimitiveTopology Topology => _currentState.Topology;
public readonly Texture[] RenderTargets => _currentState.RenderTargets;
public readonly Texture DepthStencil => _currentState.DepthStencil;
public EncoderStateManager(MTLDevice device, Pipeline pipeline) 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() public MTLRenderCommandEncoder CreateRenderCommandEncoder()
{ {
// Initialise Pass & State // Initialise Pass & State
@ -93,7 +96,7 @@ namespace Ryujinx.Graphics.Metal
{ {
var passAttachment = renderPassDescriptor.ColorAttachments.Object((ulong)i); var passAttachment = renderPassDescriptor.ColorAttachments.Object((ulong)i);
passAttachment.Texture = _currentState.RenderTargets[i].MTLTexture; passAttachment.Texture = _currentState.RenderTargets[i].MTLTexture;
passAttachment.LoadAction = MTLLoadAction.Load; passAttachment.LoadAction = _currentState.ClearLoadAction ? MTLLoadAction.Clear : MTLLoadAction.Load;
passAttachment.StoreAction = MTLStoreAction.Store; passAttachment.StoreAction = MTLStoreAction.Store;
} }
} }
@ -660,6 +663,8 @@ namespace Ryujinx.Graphics.Metal
// TODO: Handle 'zero' buffers // TODO: Handle 'zero' buffers
for (int i = 0; i < attribDescriptors.Length; i++) for (int i = 0; i < attribDescriptors.Length; i++)
{
if (!attribDescriptors[i].IsZero)
{ {
var attrib = vertexDescriptor.Attributes.Object((ulong)i); var attrib = vertexDescriptor.Attributes.Object((ulong)i);
attrib.Format = attribDescriptors[i].Format.Convert(); attrib.Format = attribDescriptors[i].Format.Convert();
@ -667,6 +672,11 @@ namespace Ryujinx.Graphics.Metal
attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex; attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex;
attrib.Offset = (ulong)attribDescriptors[i].Offset; attrib.Offset = (ulong)attribDescriptors[i].Offset;
} }
else
{
// Logger.Warning?.PrintMsg(LogClass.Gpu, "Unhandled IsZero buffer!");
}
}
for (int i = 0; i < bufferDescriptors.Length; i++) for (int i = 0; i < bufferDescriptors.Length; i++)
{ {

View file

@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.Metal
private readonly Pipeline _pipeline; private readonly Pipeline _pipeline;
private MTLDevice _device; private MTLDevice _device;
private readonly ISampler _samplerLinear;
private readonly ISampler _samplerNearest;
private readonly IProgram _programColorBlit; private readonly IProgram _programColorBlit;
private readonly List<IProgram> _programsColorClear = new(); private readonly List<IProgram> _programsColorClear = new();
private readonly IProgram _programDepthStencilClear; private readonly IProgram _programDepthStencilClear;
@ -25,6 +27,9 @@ namespace Ryujinx.Graphics.Metal
_device = device; _device = device;
_pipeline = pipeline; _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"); var blitSource = ReadMsl("Blit.metal");
_programColorBlit = new Program( _programColorBlit = new Program(
[ [
@ -56,28 +61,140 @@ namespace Ryujinx.Graphics.Metal
return EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName)); return EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName));
} }
public void BlitColor( public unsafe void BlitColor(
ITexture source, ITexture src,
ITexture destination) ITexture dst,
Extents2D srcRegion,
Extents2D dstRegion,
bool linearFilter)
{ {
var sampler = _device.NewSamplerState(new MTLSamplerDescriptor const int RegionBufferSize = 16;
var sampler = linearFilter ? _samplerLinear : _samplerNearest;
Span<float> 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, (region[0], region[1]) = (region[1], region[0]);
MagFilter = MTLSamplerMinMagFilter.Nearest, }
MipFilter = MTLSamplerMipFilter.NotMipmapped
}); if (dstRegion.Y1 > dstRegion.Y2)
{
(region[2], region[3]) = (region[3], region[2]);
}
var rect = new Rectangle<float>(
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<Viewport> 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 // Save current state
_pipeline.SaveAndResetState(); _pipeline.SaveAndResetState();
_pipeline.SetProgram(_programColorBlit); _pipeline.SetProgram(_programColorBlit);
// Viewport and scissor needs to be set before render pass begin so as not to bind the old ones _pipeline.SetViewports(viewports);
_pipeline.SetViewports([]); _pipeline.SetScissors(stackalloc Rectangle<int>[] { new Rectangle<int>(0, 0, dstWidth, dstHeight) });
_pipeline.SetScissors([]); _pipeline.SetRenderTargets([dst], null);
_pipeline.SetRenderTargets([destination], null); _pipeline.SetClearLoadAction(true);
_pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, source, new Sampler(sampler)); _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, sampler);
_pipeline.SetPrimitiveTopology(PrimitiveTopology.Triangles); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
_pipeline.Draw(6, 1, 0, 0);
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<float> 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<Viewport> viewports = stackalloc Viewport[1];
Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
var rect = new Rectangle<float>(
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<int>(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 // Restore previous state
_pipeline.RestoreState(); _pipeline.RestoreState();
@ -169,6 +286,8 @@ namespace Ryujinx.Graphics.Metal
} }
_programDepthStencilClear.Dispose(); _programDepthStencilClear.Dispose();
_pipeline.Dispose(); _pipeline.Dispose();
_samplerLinear.Dispose();
_samplerNearest.Dispose();
} }
} }
} }

View file

@ -61,6 +61,11 @@ namespace Ryujinx.Graphics.Metal
_encoderStateManager.RestoreState(); _encoderStateManager.RestoreState();
} }
public void SetClearLoadAction(bool clear)
{
_encoderStateManager.SetClearLoadAction(clear);
}
public MTLRenderCommandEncoder GetOrCreateRenderEncoder() public MTLRenderCommandEncoder GetOrCreateRenderEncoder()
{ {
MTLRenderCommandEncoder renderCommandEncoder; MTLRenderCommandEncoder renderCommandEncoder;
@ -167,22 +172,17 @@ namespace Ryujinx.Graphics.Metal
return computeCommandEncoder; 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(); EndCurrentPass();
SaveState(); SaveState();
// TODO: Clean this up // 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 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(); EndCurrentPass();
@ -194,7 +194,7 @@ namespace Ryujinx.Graphics.Metal
RestoreState(); RestoreState();
// Cleanup // Cleanup
dest.Dispose(); dst.Dispose();
} }
public void Barrier() public void Barrier()
@ -338,9 +338,7 @@ namespace Ryujinx.Graphics.Metal
public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion)
{ {
// var renderCommandEncoder = GetOrCreateRenderEncoder(); _helperShader.DrawTexture(texture, sampler, srcRegion, dstRegion);
Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!");
} }
public void SetAlphaTest(bool enable, float reference, CompareOp op) public void SetAlphaTest(bool enable, float reference, CompareOp op)

View file

@ -2,32 +2,23 @@
using namespace metal; 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 { struct CopyVertexOut {
float4 position [[position]]; float4 position [[position]];
float2 uv; float2 uv;
}; };
vertex CopyVertexOut vertexMain(unsigned short vid [[vertex_id]]) { vertex CopyVertexOut vertexMain(uint vid [[vertex_id]],
float2 position = quadVertices[vid]; const device float* texCoord [[buffer(0)]]) {
CopyVertexOut out; CopyVertexOut out;
out.position = float4(position, 0, 1); int low = vid & 1;
out.position.y = -out.position.y; int high = vid >> 1;
out.uv = position * 0.5f + 0.5f; 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; return out;
} }

View file

@ -1,5 +1,6 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Metal.Effects;
using SharpMetal.ObjectiveCCore; using SharpMetal.ObjectiveCCore;
using SharpMetal.QuartzCore; using SharpMetal.QuartzCore;
using System; using System;
@ -10,51 +11,196 @@ namespace Ryujinx.Graphics.Metal
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
class Window : IWindow, IDisposable class Window : IWindow, IDisposable
{ {
public bool ScreenCaptureRequested { get; set; }
private readonly MetalRenderer _renderer; private readonly MetalRenderer _renderer;
private readonly CAMetalLayer _metalLayer; 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) public Window(MetalRenderer renderer, CAMetalLayer metalLayer)
{ {
_renderer = renderer; _renderer = renderer;
_metalLayer = metalLayer; _metalLayer = metalLayer;
} }
// TODO: Handle ImageCrop
public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback)
{ {
if (_renderer.Pipeline is Pipeline pipeline && texture is Texture tex) if (_renderer.Pipeline is Pipeline pipeline && texture is Texture tex)
{ {
var drawable = new CAMetalDrawable(ObjectiveC.IntPtr_objc_msgSend(_metalLayer, "nextDrawable")); 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) public void SetSize(int width, int height)
{ {
Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); // Ignore
} }
public void ChangeVSyncMode(bool vsyncEnabled) 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) 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) 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() public void Dispose()
{ {