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
public DirtyFlags Dirty = new();
// Only to be used for present
public bool ClearLoadAction = false;
public EncoderState() { }
public EncoderState Clone()

View file

@ -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;
}
}
@ -660,6 +663,8 @@ namespace Ryujinx.Graphics.Metal
// TODO: Handle 'zero' buffers
for (int i = 0; i < attribDescriptors.Length; i++)
{
if (!attribDescriptors[i].IsZero)
{
var attrib = vertexDescriptor.Attributes.Object((ulong)i);
attrib.Format = attribDescriptors[i].Format.Convert();
@ -667,6 +672,11 @@ namespace Ryujinx.Graphics.Metal
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++)
{

View file

@ -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<IProgram> _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<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,
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<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
_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<int>[] { new Rectangle<int>(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<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
_pipeline.RestoreState();
@ -169,6 +286,8 @@ namespace Ryujinx.Graphics.Metal
}
_programDepthStencilClear.Dispose();
_pipeline.Dispose();
_samplerLinear.Dispose();
_samplerNearest.Dispose();
}
}
}

View file

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

View file

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

View file

@ -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()
{