Shader Extra Set Support + Cleanup (#36)

Separate samplers are now supported and arrays in constant sets are bound
This commit is contained in:
Isaac Marovitz 2024-07-31 23:32:37 +01:00
parent d9025904a7
commit 4cd15cb1a6
20 changed files with 412 additions and 181 deletions

View file

@ -2,7 +2,6 @@ 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 MaxVertexBuffers = 16;
public const int MaxUniformBuffersPerStage = 18;
@ -15,17 +14,25 @@ namespace Ryujinx.Graphics.Metal
public const int MaxViewports = 16;
// TODO: Check this value
public const int MaxVertexAttributes = 31;
// TODO: Check this value
public const int MaxVertexLayouts = 31;
public const int MinResourceAlignment = 16;
// Must match constants set in shader generation
public const uint ZeroBufferIndex = 18;
public const uint ZeroBufferIndex = MaxVertexBuffers;
public const uint BaseSetIndex = MaxVertexBuffers + 1;
public const uint ConstantBuffersIndex = 20;
public const uint StorageBuffersIndex = 21;
public const uint TexturesIndex = 22;
public const uint ImagesIndex = 23;
public const uint ConstantBuffersIndex = BaseSetIndex;
public const uint StorageBuffersIndex = BaseSetIndex + 1;
public const uint TexturesIndex = BaseSetIndex + 2;
public const uint ImagesIndex = BaseSetIndex + 3;
public const uint ConstantBuffersSetIndex = 0;
public const uint StorageBuffersSetIndex = 1;
public const uint TexturesSetIndex = 2;
public const uint ImagesSetIndex = 3;
public const uint MaximumBufferArgumentTableEntries = 31;
public const uint MaximumExtraSets = MaximumBufferArgumentTableEntries - ImagesIndex;
}
}

View file

@ -57,11 +57,16 @@ namespace Ryujinx.Graphics.Metal
_depthStencilCache.Dispose();
}
private readonly void SignalDirty(DirtyFlags flags)
{
_currentState.Dirty |= flags;
}
public EncoderState SwapState(EncoderState state, DirtyFlags flags = DirtyFlags.All)
{
_currentState = state ?? _mainState;
_currentState.Dirty |= flags;
SignalDirty(flags);
return _mainState;
}
@ -84,7 +89,7 @@ namespace Ryujinx.Graphics.Metal
_currentState.Topology = state.Topology;
_currentState.Viewports = state.Viewports;
_currentState.Dirty |= DirtyFlags.CullMode | DirtyFlags.DepthStencil | DirtyFlags.Viewports;
SignalDirty(DirtyFlags.CullMode | DirtyFlags.DepthStencil | DirtyFlags.Viewports);
}
public readonly void SetClearLoadAction(bool clear)
@ -94,12 +99,12 @@ namespace Ryujinx.Graphics.Metal
public void DirtyTextures()
{
_currentState.Dirty |= DirtyFlags.Textures;
SignalDirty(DirtyFlags.Textures);
}
public void DirtyImages()
{
_currentState.Dirty |= DirtyFlags.Images;
SignalDirty(DirtyFlags.Images);
}
public readonly MTLRenderCommandEncoder CreateRenderCommandEncoder()
@ -161,7 +166,7 @@ namespace Ryujinx.Graphics.Metal
var renderCommandEncoder = _pipeline.CommandBuffer.RenderCommandEncoder(renderPassDescriptor);
// Mark all state as dirty to ensure it is set on the encoder
_currentState.Dirty |= DirtyFlags.RenderAll;
SignalDirty(DirtyFlags.RenderAll);
// Cleanup
renderPassDescriptor.Dispose();
@ -175,7 +180,7 @@ namespace Ryujinx.Graphics.Metal
var computeCommandEncoder = _pipeline.CommandBuffer.ComputeCommandEncoder(descriptor);
// Mark all state as dirty to ensure it is set on the encoder
_currentState.Dirty |= DirtyFlags.ComputeAll;
SignalDirty(DirtyFlags.ComputeAll);
// Cleanup
descriptor.Dispose();
@ -233,22 +238,22 @@ namespace Ryujinx.Graphics.Metal
if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0)
{
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, MetalRenderer.UniformSetIndex);
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, Constants.ConstantBuffersSetIndex);
}
if ((_currentState.Dirty & DirtyFlags.Storages) != 0)
{
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, MetalRenderer.StorageSetIndex);
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, Constants.StorageBuffersSetIndex);
}
if ((_currentState.Dirty & DirtyFlags.Textures) != 0)
{
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, MetalRenderer.TextureSetIndex);
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, Constants.TexturesSetIndex);
}
if (_currentState.Dirty.HasFlag(DirtyFlags.Images))
if ((_currentState.Dirty & DirtyFlags.Images) != 0)
{
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, MetalRenderer.ImageSetIndex);
UpdateAndBind(renderCommandEncoder, _currentState.RenderProgram, Constants.ImagesSetIndex);
}
_currentState.Dirty &= ~DirtyFlags.RenderAll;
@ -256,29 +261,29 @@ namespace Ryujinx.Graphics.Metal
public readonly void RebindComputeState(MTLComputeCommandEncoder computeCommandEncoder)
{
if (_currentState.Dirty.HasFlag(DirtyFlags.ComputePipeline))
if ((_currentState.Dirty & DirtyFlags.ComputePipeline) != 0)
{
SetComputePipelineState(computeCommandEncoder);
}
if (_currentState.Dirty.HasFlag(DirtyFlags.Uniforms))
if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0)
{
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, MetalRenderer.UniformSetIndex);
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, Constants.ConstantBuffersSetIndex);
}
if (_currentState.Dirty.HasFlag(DirtyFlags.Storages))
if ((_currentState.Dirty & DirtyFlags.Storages) != 0)
{
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, MetalRenderer.StorageSetIndex);
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, Constants.StorageBuffersSetIndex);
}
if (_currentState.Dirty.HasFlag(DirtyFlags.Textures))
if ((_currentState.Dirty & DirtyFlags.Textures) != 0)
{
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, MetalRenderer.TextureSetIndex);
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, Constants.TexturesSetIndex);
}
if (_currentState.Dirty.HasFlag(DirtyFlags.Images))
if ((_currentState.Dirty & DirtyFlags.Images) != 0)
{
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, MetalRenderer.ImageSetIndex);
UpdateAndBind(computeCommandEncoder, _currentState.ComputeProgram, Constants.ImagesSetIndex);
}
_currentState.Dirty &= ~DirtyFlags.ComputeAll;
@ -347,13 +352,13 @@ namespace Ryujinx.Graphics.Metal
{
_currentState.RenderProgram = prg;
_currentState.Dirty |= DirtyFlags.RenderPipeline | DirtyFlags.ArgBuffers;
SignalDirty(DirtyFlags.RenderPipeline | DirtyFlags.ArgBuffers);
}
else if (prg.ComputeFunction != IntPtr.Zero)
{
_currentState.ComputeProgram = prg;
_currentState.Dirty |= DirtyFlags.ComputePipeline | DirtyFlags.ArgBuffers;
SignalDirty(DirtyFlags.ComputePipeline | DirtyFlags.ArgBuffers);
}
}
@ -516,8 +521,7 @@ namespace Ryujinx.Graphics.Metal
// Update the buffers on the pipeline
UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs);
// Mark dirty
_currentState.Dirty |= DirtyFlags.RenderPipeline;
SignalDirty(DirtyFlags.RenderPipeline);
}
public readonly void UpdateBlendDescriptors(int index, BlendDescriptor blend)
@ -541,11 +545,9 @@ namespace Ryujinx.Graphics.Metal
_currentState.BlendColor = blend.BlendConstant;
// Mark dirty
_currentState.Dirty |= DirtyFlags.RenderPipeline;
SignalDirty(DirtyFlags.RenderPipeline);
}
// Inlineable
public void UpdateStencilState(StencilTestDescriptor stencilTest)
{
ref DepthStencilUid uid = ref _currentState.DepthStencilUid;
@ -574,8 +576,7 @@ namespace Ryujinx.Graphics.Metal
UpdateStencilRefValue(stencilTest.FrontFuncRef, stencilTest.BackFuncRef);
// Mark dirty
_currentState.Dirty |= DirtyFlags.DepthStencil;
SignalDirty(DirtyFlags.DepthStencil);
}
public readonly void UpdateDepthState(DepthTestDescriptor depthTest)
@ -585,11 +586,9 @@ namespace Ryujinx.Graphics.Metal
uid.DepthCompareFunction = depthTest.TestEnable ? depthTest.Func.Convert() : MTLCompareFunction.Always;
uid.DepthWriteEnabled = depthTest.TestEnable && depthTest.WriteEnable;
// Mark dirty
_currentState.Dirty |= DirtyFlags.DepthStencil;
SignalDirty(DirtyFlags.DepthStencil);
}
// Inlineable
public readonly void UpdateDepthClamp(bool clamp)
{
_currentState.DepthClipMode = clamp ? MTLDepthClipMode.Clamp : MTLDepthClipMode.Clip;
@ -601,11 +600,9 @@ namespace Ryujinx.Graphics.Metal
return;
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.DepthClamp;
SignalDirty(DirtyFlags.DepthClamp);
}
// Inlineable
public readonly void UpdateDepthBias(float depthBias, float slopeScale, float clamp)
{
_currentState.DepthBias = depthBias;
@ -619,11 +616,9 @@ namespace Ryujinx.Graphics.Metal
return;
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.DepthBias;
SignalDirty(DirtyFlags.DepthBias);
}
// Inlineable
public void UpdateScissors(ReadOnlySpan<Rectangle<int>> regions)
{
for (int i = 0; i < regions.Length; i++)
@ -646,11 +641,9 @@ namespace Ryujinx.Graphics.Metal
return;
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.Scissors;
SignalDirty(DirtyFlags.Scissors);
}
// Inlineable
public void UpdateViewports(ReadOnlySpan<Viewport> viewports)
{
static float Clamp(float value)
@ -680,8 +673,7 @@ namespace Ryujinx.Graphics.Metal
return;
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.Viewports;
SignalDirty(DirtyFlags.Viewports);
}
public readonly void UpdateVertexBuffers(ReadOnlySpan<VertexBufferDescriptor> vertexBuffers)
@ -708,8 +700,7 @@ namespace Ryujinx.Graphics.Metal
// Update the buffers on the pipeline
UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs);
// Mark dirty
_currentState.Dirty |= DirtyFlags.RenderPipeline;
SignalDirty(DirtyFlags.RenderPipeline);
}
public readonly void UpdateUniformBuffers(ReadOnlySpan<BufferAssignment> buffers)
@ -726,7 +717,7 @@ namespace Ryujinx.Graphics.Metal
_currentState.UniformBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer);
}
_currentState.Dirty |= DirtyFlags.Uniforms;
SignalDirty(DirtyFlags.Uniforms);
}
public readonly void UpdateStorageBuffers(ReadOnlySpan<BufferAssignment> buffers)
@ -743,7 +734,7 @@ namespace Ryujinx.Graphics.Metal
_currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer);
}
_currentState.Dirty |= DirtyFlags.Storages;
SignalDirty(DirtyFlags.Storages);
}
public readonly void UpdateStorageBuffers(int first, ReadOnlySpan<Auto<DisposableBuffer>> buffers)
@ -756,10 +747,9 @@ namespace Ryujinx.Graphics.Metal
_currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer);
}
_currentState.Dirty |= DirtyFlags.Storages;
SignalDirty(DirtyFlags.Storages);
}
// Inlineable
public void UpdateCullMode(bool enable, Face face)
{
var dirtyScissor = (face == Face.FrontAndBack) != _currentState.CullBoth;
@ -776,15 +766,14 @@ namespace Ryujinx.Graphics.Metal
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.CullMode;
SignalDirty(DirtyFlags.CullMode);
if (dirtyScissor)
{
_currentState.Dirty |= DirtyFlags.Scissors;
SignalDirty(DirtyFlags.Scissors);
}
}
// Inlineable
public readonly void UpdateFrontFace(FrontFace frontFace)
{
_currentState.Winding = frontFace.Convert();
@ -796,8 +785,7 @@ namespace Ryujinx.Graphics.Metal
return;
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.FrontFace;
SignalDirty(DirtyFlags.FrontFace);
}
private readonly void UpdateStencilRefValue(int frontRef, int backRef)
@ -811,8 +799,7 @@ namespace Ryujinx.Graphics.Metal
SetStencilRefValue(renderCommandEncoder);
}
// Mark dirty
_currentState.Dirty |= DirtyFlags.StencilRef;
SignalDirty(DirtyFlags.StencilRef);
}
public readonly void UpdateTextureAndSampler(ShaderStage stage, ulong binding, TextureBase texture, Sampler sampler)
@ -826,7 +813,7 @@ namespace Ryujinx.Graphics.Metal
_currentState.TextureRefs[binding] = default;
}
_currentState.Dirty |= DirtyFlags.Textures;
SignalDirty(DirtyFlags.Textures);
}
public readonly void UpdateImage(ShaderStage stage, ulong binding, TextureBase texture)
@ -840,7 +827,7 @@ namespace Ryujinx.Graphics.Metal
_currentState.ImageRefs[binding] = default;
}
_currentState.Dirty |= DirtyFlags.Images;
SignalDirty(DirtyFlags.Images);
}
public void UpdateTextureArray(ShaderStage stage, ulong binding, TextureArray array)
@ -851,19 +838,19 @@ namespace Ryujinx.Graphics.Metal
{
arrayRef = new EncoderState.ArrayRef<TextureArray>(stage, array);
_currentState.Dirty |= DirtyFlags.Textures;
SignalDirty(DirtyFlags.Textures);
}
}
public void UpdateTextureArraySeparate(ShaderStage stage, int setIndex, TextureArray array)
{
ref EncoderState.ArrayRef<TextureArray> arrayRef = ref GetArrayRef(ref _currentState.TextureArrayRefs, setIndex);
ref EncoderState.ArrayRef<TextureArray> arrayRef = ref GetArrayRef(ref _currentState.TextureArrayExtraRefs, setIndex - MetalRenderer.TotalSets);
if (arrayRef.Stage != stage || arrayRef.Array != array)
{
arrayRef = new EncoderState.ArrayRef<TextureArray>(stage, array);
_currentState.Dirty |= DirtyFlags.Textures;
SignalDirty(DirtyFlags.Textures);
}
}
@ -875,19 +862,19 @@ namespace Ryujinx.Graphics.Metal
{
arrayRef = new EncoderState.ArrayRef<ImageArray>(stage, array);
_currentState.Dirty |= DirtyFlags.Images;
SignalDirty(DirtyFlags.Images);
}
}
public void UpdateImageArraySeparate(ShaderStage stage, int setIndex, ImageArray array)
{
ref EncoderState.ArrayRef<ImageArray> arrayRef = ref GetArrayRef(ref _currentState.ImageArrayExtraRefs, setIndex);
ref EncoderState.ArrayRef<ImageArray> arrayRef = ref GetArrayRef(ref _currentState.ImageArrayExtraRefs, setIndex - MetalRenderer.TotalSets);
if (arrayRef.Stage != stage || arrayRef.Array != array)
{
arrayRef = new EncoderState.ArrayRef<ImageArray>(stage, array);
_currentState.Dirty |= DirtyFlags.Images;
SignalDirty(DirtyFlags.Images);
}
}
@ -1054,7 +1041,7 @@ namespace Ryujinx.Graphics.Metal
renderCommandEncoder.SetVertexBuffer(zeroMtlBuffer, 0, Constants.ZeroBufferIndex);
}
private readonly void UpdateAndBind(MTLRenderCommandEncoder renderCommandEncoder, Program program, int setIndex)
private readonly void UpdateAndBind(MTLRenderCommandEncoder renderCommandEncoder, Program program, uint setIndex)
{
var bindingSegments = program.BindingSegments[setIndex];
@ -1089,7 +1076,7 @@ namespace Ryujinx.Graphics.Metal
switch (setIndex)
{
case MetalRenderer.UniformSetIndex:
case Constants.ConstantBuffersSetIndex:
for (int i = 0; i < count; i++)
{
int index = binding + i;
@ -1139,7 +1126,7 @@ namespace Ryujinx.Graphics.Metal
renderCommandEncoder.UseResource(new MTLResource(mtlBuffer.NativePtr), MTLResourceUsage.Read, renderStages);
}
break;
case MetalRenderer.StorageSetIndex:
case Constants.StorageBuffersSetIndex:
for (int i = 0; i < count; i++)
{
int index = binding + i;
@ -1170,7 +1157,7 @@ namespace Ryujinx.Graphics.Metal
MTLRenderStages renderStages = 0;
if (segment.Stages.HasFlag(ResourceStages.Vertex))
if ((segment.Stages & ResourceStages.Vertex) != 0)
{
vertResourceIds[vertResourceIdIndex] = mtlBuffer.GpuAddress + (ulong)offset;
vertResourceIdIndex++;
@ -1178,7 +1165,7 @@ namespace Ryujinx.Graphics.Metal
renderStages |= MTLRenderStages.RenderStageVertex;
}
if (segment.Stages.HasFlag(ResourceStages.Fragment))
if ((segment.Stages & ResourceStages.Fragment) != 0)
{
fragResourceIds[fragResourceIdIndex] = mtlBuffer.GpuAddress + (ulong)offset;
fragResourceIdIndex++;
@ -1189,7 +1176,7 @@ namespace Ryujinx.Graphics.Metal
renderCommandEncoder.UseResource(new MTLResource(mtlBuffer.NativePtr), MTLResourceUsage.Read, renderStages);
}
break;
case MetalRenderer.TextureSetIndex:
case Constants.TexturesSetIndex:
if (!segment.IsArray)
{
for (int i = 0; i < count; i++)
@ -1247,10 +1234,106 @@ namespace Ryujinx.Graphics.Metal
}
else
{
// TODO: Texture arrays
var textureArray = _currentState.TextureArrayRefs[binding].Array;
if (segment.Type != ResourceType.BufferTexture)
{
var textures = textureArray.GetTextureRefs();
var samplers = new Sampler[textures.Length];
for (int i = 0; i < textures.Length; i++)
{
TextureRef texture = textures[i];
if (texture.Storage == null)
{
continue;
}
var mtlTexture = texture.Storage.GetHandle();
samplers[i] = texture.Sampler;
MTLRenderStages renderStages = 0;
if ((segment.Stages & ResourceStages.Vertex) != 0)
{
vertResourceIds[vertResourceIdIndex] = mtlTexture.GpuResourceID._impl;
vertResourceIdIndex++;
renderStages |= MTLRenderStages.RenderStageVertex;
}
if ((segment.Stages & ResourceStages.Fragment) != 0)
{
fragResourceIds[fragResourceIdIndex] = mtlTexture.GpuResourceID._impl;
fragResourceIdIndex++;
renderStages |= MTLRenderStages.RenderStageFragment;
}
renderCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr),
MTLResourceUsage.Read, renderStages);
}
foreach (var sampler in samplers)
{
if (sampler == null)
{
continue;
}
if ((segment.Stages & ResourceStages.Vertex) != 0)
{
vertResourceIds[vertResourceIdIndex] = sampler.GetSampler().GpuResourceID._impl;
vertResourceIdIndex++;
}
if ((segment.Stages & ResourceStages.Fragment) != 0)
{
fragResourceIds[fragResourceIdIndex] = sampler.GetSampler().GpuResourceID._impl;
fragResourceIdIndex++;
}
}
}
else
{
var bufferTextures = textureArray.GetBufferTextureRefs();
foreach (TextureBuffer bufferTexture in bufferTextures)
{
if (bufferTexture == null)
{
continue;
}
bufferTexture.RebuildStorage(false);
var mtlTexture = bufferTexture.GetHandle();
MTLRenderStages renderStages = 0;
if ((segment.Stages & ResourceStages.Vertex) != 0)
{
vertResourceIds[vertResourceIdIndex] = mtlTexture.GpuResourceID._impl;
vertResourceIdIndex++;
renderStages |= MTLRenderStages.RenderStageVertex;
}
if ((segment.Stages & ResourceStages.Fragment) != 0)
{
fragResourceIds[fragResourceIdIndex] = mtlTexture.GpuResourceID._impl;
fragResourceIdIndex++;
renderStages |= MTLRenderStages.RenderStageFragment;
}
renderCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr), MTLResourceUsage.Read, renderStages);
}
}
}
break;
case MetalRenderer.ImageSetIndex:
case Constants.ImagesSetIndex:
if (!segment.IsArray)
{
for (int i = 0; i < count; i++)
@ -1306,7 +1389,7 @@ namespace Ryujinx.Graphics.Metal
}
}
private readonly void UpdateAndBind(MTLComputeCommandEncoder computeCommandEncoder, Program program, int setIndex)
private readonly void UpdateAndBind(MTLComputeCommandEncoder computeCommandEncoder, Program program, uint setIndex)
{
var bindingSegments = program.BindingSegments[setIndex];
@ -1332,7 +1415,7 @@ namespace Ryujinx.Graphics.Metal
switch (setIndex)
{
case MetalRenderer.UniformSetIndex:
case Constants.ConstantBuffersSetIndex:
for (int i = 0; i < count; i++)
{
int index = binding + i;
@ -1369,7 +1452,7 @@ namespace Ryujinx.Graphics.Metal
}
}
break;
case MetalRenderer.StorageSetIndex:
case Constants.StorageBuffersSetIndex:
for (int i = 0; i < count; i++)
{
int index = binding + i;
@ -1406,7 +1489,7 @@ namespace Ryujinx.Graphics.Metal
}
}
break;
case MetalRenderer.TextureSetIndex:
case Constants.TexturesSetIndex:
if (!segment.IsArray)
{
for (int i = 0; i < count; i++)
@ -1429,7 +1512,7 @@ namespace Ryujinx.Graphics.Metal
var mtlTexture = storage.GetHandle();
if (segment.Stages.HasFlag(ResourceStages.Compute))
if ((segment.Stages & ResourceStages.Compute) != 0)
{
computeCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr), MTLResourceUsage.Read);
resourceIds[resourceIdIndex] = mtlTexture.GpuResourceID._impl;
@ -1445,10 +1528,70 @@ namespace Ryujinx.Graphics.Metal
}
else
{
// TODO: Texture arrays
var textureArray = _currentState.TextureArrayRefs[binding].Array;
if (segment.Type != ResourceType.BufferTexture)
{
var textures = textureArray.GetTextureRefs();
var samplers = new Sampler[textures.Length];
for (int i = 0; i < textures.Length; i++)
{
TextureRef texture = textures[i];
if (texture.Storage == null)
{
continue;
}
var mtlTexture = texture.Storage.GetHandle();
if ((segment.Stages & ResourceStages.Compute) != 0)
{
computeCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr),
MTLResourceUsage.Read);
resourceIds[resourceIdIndex] = mtlTexture.GpuResourceID._impl;
resourceIdIndex++;
samplers[i] = texture.Sampler;
}
}
foreach (var sampler in samplers)
{
if (sampler != null)
{
resourceIds[resourceIdIndex] = sampler.GetSampler().GpuResourceID._impl;
resourceIdIndex++;
}
}
}
else
{
var bufferTextures = textureArray.GetBufferTextureRefs();
foreach (TextureBuffer bufferTexture in bufferTextures)
{
if (bufferTexture == null)
{
continue;
}
bufferTexture.RebuildStorage(false);
var mtlTexture = bufferTexture.GetHandle();
if ((segment.Stages & ResourceStages.Compute) != 0)
{
computeCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr), MTLResourceUsage.Read);
resourceIds[resourceIdIndex] = mtlTexture.GpuResourceID._impl;
resourceIdIndex++;
}
}
}
}
break;
case MetalRenderer.ImageSetIndex:
case Constants.ImagesSetIndex:
if (!segment.IsArray)
{
if (segment.Type != ResourceType.BufferTexture)
@ -1468,7 +1611,7 @@ namespace Ryujinx.Graphics.Metal
var mtlTexture = storage.GetHandle();
if (segment.Stages.HasFlag(ResourceStages.Compute))
if ((segment.Stages & ResourceStages.Compute) != 0)
{
computeCommandEncoder.UseResource(new MTLResource(mtlTexture.NativePtr), MTLResourceUsage.Read | MTLResourceUsage.Write);
resourceIds[resourceIdIndex] = mtlTexture.GpuResourceID._impl;
@ -1489,14 +1632,14 @@ namespace Ryujinx.Graphics.Metal
}
}
private static uint SetIndexToBindingIndex(int setIndex)
private static uint SetIndexToBindingIndex(uint setIndex)
{
return setIndex switch
{
MetalRenderer.UniformSetIndex => Constants.ConstantBuffersIndex,
MetalRenderer.StorageSetIndex => Constants.StorageBuffersIndex,
MetalRenderer.TextureSetIndex => Constants.TexturesIndex,
MetalRenderer.ImageSetIndex => Constants.ImagesIndex,
Constants.ConstantBuffersSetIndex => Constants.ConstantBuffersIndex,
Constants.StorageBuffersSetIndex => Constants.StorageBuffersIndex,
Constants.TexturesSetIndex => Constants.TexturesIndex,
Constants.ImagesSetIndex => Constants.ImagesIndex,
};
}

View file

@ -123,7 +123,16 @@ namespace Ryujinx.Graphics.Metal
private static string ReadMsl(string fileName)
{
return EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName));
var msl = EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName));
#pragma warning disable IDE0055 // Disable formatting
msl = msl.Replace("CONSTANT_BUFFERS_INDEX", $"{Constants.ConstantBuffersIndex}")
.Replace("STORAGE_BUFFERS_INDEX", $"{Constants.StorageBuffersIndex}")
.Replace("TEXTURES_INDEX", $"{Constants.TexturesIndex}")
.Replace("IMAGES_INDEX", $"{Constants.ImagesIndex}");
#pragma warning restore IDE0055
return msl;
}
public unsafe void BlitColor(

View file

@ -13,11 +13,6 @@ namespace Ryujinx.Graphics.Metal
{
public const int TotalSets = 4;
public const int UniformSetIndex = 0;
public const int StorageSetIndex = 1;
public const int TextureSetIndex = 2;
public const int ImageSetIndex = 3;
private readonly MTLDevice _device;
private readonly MTLCommandQueue _queue;
private readonly Func<CAMetalLayer> _getMetalLayer;
@ -181,8 +176,7 @@ namespace Ryujinx.Graphics.Metal
supportsCubemapView: true,
supportsNonConstantTextureOffset: false,
supportsQuads: false,
// TODO: Metal Bindless Support
supportsSeparateSampler: false,
supportsSeparateSampler: true,
supportsShaderBallot: false,
supportsShaderBarrierDivergence: false,
supportsShaderFloat64: false,
@ -194,12 +188,12 @@ namespace Ryujinx.Graphics.Metal
supportsViewportSwizzle: false,
supportsIndirectParameters: true,
supportsDepthClipControl: false,
uniformBufferSetIndex: UniformSetIndex,
storageBufferSetIndex: StorageSetIndex,
textureSetIndex: TextureSetIndex,
imageSetIndex: ImageSetIndex,
extraSetBaseIndex: 0,
maximumExtraSets: 0,
uniformBufferSetIndex: (int)Constants.ConstantBuffersSetIndex,
storageBufferSetIndex: (int)Constants.StorageBuffersSetIndex,
textureSetIndex: (int)Constants.TexturesSetIndex,
imageSetIndex: (int)Constants.ImagesSetIndex,
extraSetBaseIndex: TotalSets,
maximumExtraSets: (int)Constants.MaximumExtraSets,
maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage,
maximumTexturesPerStage: Constants.MaxTexturesPerStage,

View file

@ -27,12 +27,12 @@ namespace Ryujinx.Graphics.Metal
public ResourceLayoutBuilder Add(ResourceStages stages, ResourceType type, int binding, bool write = false)
{
int setIndex = type switch
uint setIndex = type switch
{
ResourceType.UniformBuffer => MetalRenderer.UniformSetIndex,
ResourceType.StorageBuffer => MetalRenderer.StorageSetIndex,
ResourceType.TextureAndSampler or ResourceType.BufferTexture => MetalRenderer.TextureSetIndex,
ResourceType.Image or ResourceType.BufferImage => MetalRenderer.ImageSetIndex,
ResourceType.UniformBuffer => Constants.ConstantBuffersSetIndex,
ResourceType.StorageBuffer => Constants.StorageBuffersSetIndex,
ResourceType.TextureAndSampler or ResourceType.BufferTexture => Constants.TexturesSetIndex,
ResourceType.Image or ResourceType.BufferImage => Constants.ImagesSetIndex,
_ => throw new ArgumentException($"Invalid resource type \"{type}\"."),
};

View file

@ -22,7 +22,7 @@ struct Textures
};
vertex CopyVertexOut vertexMain(uint vid [[vertex_id]],
constant ConstantBuffers &constant_buffers [[buffer(20)]]) {
constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
CopyVertexOut out;
int low = vid & 1;
@ -38,6 +38,6 @@ vertex CopyVertexOut vertexMain(uint vid [[vertex_id]],
}
fragment float4 fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]]) {
constant Textures &textures [[buffer(TEXTURES_INDEX)]]) {
return textures.texture.sample(textures.sampler, in.uv);
}

View file

@ -13,7 +13,7 @@ struct Textures
};
fragment float4 fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]],
constant Textures &textures [[buffer(TEXTURES_INDEX)]],
uint sample_id [[sample_id]]) {
uint2 tex_size = uint2(textures.texture.get_width(), textures.texture.get_height());
uint2 tex_coord = uint2(in.uv * float2(tex_size));

View file

@ -23,8 +23,8 @@ struct StorageBuffers {
device OutData* out_data;
};
kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(20)]],
device StorageBuffers &storage_buffers [[buffer(21)]],
kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]],
device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]],
uint3 thread_position_in_grid [[thread_position_in_grid]],
uint3 threads_per_threadgroup [[threads_per_threadgroup]],
uint3 threadgroups_per_grid [[threads_per_grid]])

View file

@ -33,6 +33,6 @@ struct FragmentOut {
};
fragment FragmentOut fragmentMain(VertexOut in [[stage_in]],
constant ConstantBuffers &constant_buffers [[buffer(20)]]) {
constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
return {constant_buffers.clear_color->data};
}

View file

@ -18,7 +18,7 @@ struct FragmentOut {
};
fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]]) {
constant Textures &textures [[buffer(TEXTURES_INDEX)]]) {
FragmentOut out;
out.depth = textures.texture.sample(textures.sampler, in.uv).r;

View file

@ -17,7 +17,7 @@ struct FragmentOut {
};
fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]],
constant Textures &textures [[buffer(TEXTURES_INDEX)]],
uint sample_id [[sample_id]]) {
FragmentOut out;

View file

@ -33,7 +33,7 @@ vertex VertexOut vertexMain(ushort vid [[vertex_id]]) {
}
fragment FragmentOut fragmentMain(VertexOut in [[stage_in]],
constant ConstantBuffers &constant_buffers [[buffer(20)]]) {
constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) {
FragmentOut out;
out.depth = constant_buffers.clear_depth->data;

View file

@ -18,7 +18,7 @@ struct FragmentOut {
};
fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]]) {
constant Textures &textures [[buffer(TEXTURES_INDEX)]]) {
FragmentOut out;
out.stencil = textures.texture.sample(textures.sampler, in.uv).r;

View file

@ -17,7 +17,7 @@ struct FragmentOut {
};
fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]],
constant Textures &textures [[buffer(22)]],
constant Textures &textures [[buffer(TEXTURES_INDEX)]],
uint sample_id [[sample_id]]) {
FragmentOut out;

View file

@ -73,6 +73,16 @@ namespace Ryujinx.Graphics.Metal
SetDirty();
}
public TextureRef[] GetTextureRefs()
{
return _textureRefs;
}
public TextureBuffer[] GetBufferTextureRefs()
{
return _bufferTextureRefs;
}
private void SetDirty()
{
_pipeline.DirtyTextures();

View file

@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
*
*/
public static void Declare(CodeGenContext context, StructuredProgramInfo info)
public static int[] Declare(CodeGenContext context, StructuredProgramInfo info)
{
// TODO: Re-enable this warning
context.AppendLine("#pragma clang diagnostic ignored \"-Wunused-variable\"");
@ -75,10 +75,32 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
context.AppendLine();
DeclareOutputAttributes(context, info.IoDefinitions.Where(x => x.StorageKind == StorageKind.Output));
context.AppendLine();
DeclareBufferStructures(context, context.Properties.ConstantBuffers.Values, true, fsi);
DeclareBufferStructures(context, context.Properties.StorageBuffers.Values, false, fsi);
DeclareTextures(context, context.Properties.Textures.Values);
DeclareImages(context, context.Properties.Images.Values, fsi);
DeclareBufferStructures(context, context.Properties.ConstantBuffers.Values.OrderBy(x => x.Binding).ToArray(), true, fsi);
DeclareBufferStructures(context, context.Properties.StorageBuffers.Values.OrderBy(x => x.Binding).ToArray(), false, fsi);
// We need to declare each set as a new struct
var textureDefinitions = context.Properties.Textures.Values
.GroupBy(x => x.Set)
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Binding).ToArray());
var imageDefinitions = context.Properties.Images.Values
.GroupBy(x => x.Set)
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Binding).ToArray());
var textureSets = textureDefinitions.Keys.ToArray();
var imageSets = imageDefinitions.Keys.ToArray();
var sets = textureSets.Union(imageSets).ToArray();
foreach (var set in textureDefinitions)
{
DeclareTextures(context, set.Value, set.Key);
}
foreach (var set in imageDefinitions)
{
DeclareImages(context, set.Value, set.Key, fsi);
}
if ((info.HelperFunctionsMask & HelperFunctionsMask.FindLSB) != 0)
{
@ -99,6 +121,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
{
AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal");
}
return sets;
}
static bool IsUserDefined(IoDefinition ioDefinition, StorageKind storageKind)
@ -186,22 +210,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
}
}
private static void DeclareBufferStructures(CodeGenContext context, IEnumerable<BufferDefinition> buffers, bool constant, bool fsi)
private static void DeclareBufferStructures(CodeGenContext context, BufferDefinition[] buffers, bool constant, bool fsi)
{
var name = constant ? "ConstantBuffers" : "StorageBuffers";
var addressSpace = constant ? "constant" : "device";
List<string> argBufferPointers = [];
string[] bufferDec = new string[buffers.Length];
// TODO: Avoid Linq if we can
var sortedBuffers = buffers.OrderBy(x => x.Binding).ToArray();
foreach (BufferDefinition buffer in sortedBuffers)
for (int i = 0; i < buffers.Length; i++)
{
BufferDefinition buffer = buffers[i];
var needsPadding = buffer.Layout == BufferLayout.Std140;
string fsiSuffix = constant && fsi ? " [[raster_order_group(0)]]" : "";
argBufferPointers.Add($"{addressSpace} {Defaults.StructPrefix}_{buffer.Name}* {buffer.Name}{fsiSuffix};");
bufferDec[i] = $"{addressSpace} {Defaults.StructPrefix}_{buffer.Name}* {buffer.Name}{fsiSuffix};";
context.AppendLine($"struct {Defaults.StructPrefix}_{buffer.Name}");
context.EnterScope();
@ -209,7 +232,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
foreach (StructureField field in buffer.Type.Fields)
{
var type = field.Type;
type |= (needsPadding && (field.Type & AggregateType.Array) != 0) ? AggregateType.Vector4 : AggregateType.Invalid;
type |= (needsPadding && (field.Type & AggregateType.Array) != 0)
? AggregateType.Vector4
: AggregateType.Invalid;
type &= ~AggregateType.Array;
@ -239,66 +264,85 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
context.AppendLine($"struct {name}");
context.EnterScope();
foreach (var pointer in argBufferPointers)
foreach (var declaration in bufferDec)
{
context.AppendLine(pointer);
context.AppendLine(declaration);
}
context.LeaveScope(";");
context.AppendLine();
}
private static void DeclareTextures(CodeGenContext context, IEnumerable<TextureDefinition> textures)
private static void DeclareTextures(CodeGenContext context, TextureDefinition[] textures, int set)
{
context.AppendLine("struct Textures");
var setName = GetNameForSet(set);
context.AppendLine($"struct {setName}");
context.EnterScope();
List<string> argBufferPointers = [];
List<string> textureDec = [];
// TODO: Avoid Linq if we can
var sortedTextures = textures.OrderBy(x => x.Binding).ToArray();
foreach (TextureDefinition texture in sortedTextures)
foreach (TextureDefinition texture in textures)
{
var textureTypeName = texture.Type.ToMslTextureType();
argBufferPointers.Add($"{textureTypeName} tex_{texture.Name};");
if (texture.Type != SamplerType.None)
{
var textureTypeName = texture.Type.ToMslTextureType();
if (texture.ArrayLength > 1)
{
textureTypeName = $"array<{textureTypeName}, {texture.ArrayLength}>";
}
textureDec.Add($"{textureTypeName} tex_{texture.Name};");
}
if (!texture.Separate && texture.Type != SamplerType.TextureBuffer)
{
argBufferPointers.Add($"sampler samp_{texture.Name};");
var samplerType = "sampler";
if (texture.ArrayLength > 1)
{
samplerType = $"array<{samplerType}, {texture.ArrayLength}>";
}
textureDec.Add($"{samplerType} samp_{texture.Name};");
}
}
foreach (var pointer in argBufferPointers)
foreach (var declaration in textureDec)
{
context.AppendLine(pointer);
context.AppendLine(declaration);
}
context.LeaveScope(";");
context.AppendLine();
}
private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> images, bool fsi)
private static void DeclareImages(CodeGenContext context, TextureDefinition[] images, int set, bool fsi)
{
context.AppendLine("struct Images");
var setName = GetNameForSet(set);
context.AppendLine($"struct {setName}");
context.EnterScope();
List<string> argBufferPointers = [];
string[] imageDec = new string[images.Length];
// TODO: Avoid Linq if we can
var sortedImages = images.OrderBy(x => x.Binding).ToArray();
foreach (TextureDefinition image in sortedImages)
for (int i = 0; i < images.Length; i++)
{
TextureDefinition image = images[i];
var imageTypeName = image.Type.ToMslTextureType(true);
if (image.ArrayLength > 1)
{
imageTypeName = $"array<{imageTypeName}, {image.ArrayLength}>";
}
string fsiSuffix = fsi ? " [[raster_order_group(0)]]" : "";
argBufferPointers.Add($"{imageTypeName} {image.Name}{fsiSuffix};");
imageDec[i] = $"{imageTypeName} {image.Name}{fsiSuffix};";
}
foreach (var pointer in argBufferPointers)
foreach (var declaration in imageDec)
{
context.AppendLine(pointer);
context.AppendLine(declaration);
}
context.LeaveScope(";");
@ -483,5 +527,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
context.AppendLine(code);
context.AppendLine();
}
public static string GetNameForSet(int set, bool forVar = false)
{
return (uint)set switch
{
Defaults.TexturesSetIndex => forVar ? "textures" : "Textures",
Defaults.ImagesSetIndex => forVar ? "images" : "Images",
_ => $"{(forVar ? "set" : "Set")}{set}"
};
}
}
}

View file

@ -14,14 +14,20 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
public const string UndefinedName = "0";
public const int MaxUniformBuffersPerStage = 18;
public const int MaxStorageBuffersPerStage = 16;
public const int MaxTexturesPerStage = 64;
public const int MaxVertexBuffers = 16;
public const uint ConstantBuffersIndex = 20;
public const uint StorageBuffersIndex = 21;
public const uint TexturesIndex = 22;
public const uint ImagesIndex = 23;
public const uint ZeroBufferIndex = MaxVertexBuffers;
public const uint BaseSetIndex = MaxVertexBuffers + 1;
public const uint ConstantBuffersIndex = BaseSetIndex;
public const uint StorageBuffersIndex = BaseSetIndex + 1;
public const uint TexturesIndex = BaseSetIndex + 2;
public const uint ImagesIndex = BaseSetIndex + 3;
public const uint ConstantBuffersSetIndex = 0;
public const uint StorageBuffersSetIndex = 1;
public const uint TexturesSetIndex = 2;
public const uint ImagesSetIndex = 3;
public const int TotalClipDistances = 8;
}

View file

@ -494,13 +494,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions
{
TextureDefinition textureDefinition = context.Properties.Textures[texOp.GetTextureSetAndBinding()];
string name = textureDefinition.Name;
string setName = Declarations.GetNameForSet(textureDefinition.Set, true);
if (textureDefinition.ArrayLength != 1)
{
name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]";
}
return $"textures.tex_{name}";
return $"{setName}.tex_{name}";
}
private static string GetSamplerName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex)
@ -510,26 +511,28 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions
TextureDefinition samplerDefinition = context.Properties.Textures[index];
string name = samplerDefinition.Name;
string setName = Declarations.GetNameForSet(samplerDefinition.Set, true);
if (samplerDefinition.ArrayLength != 1)
{
name = $"{name}[{GetSourceExpr(context, texOp.GetSource(sourceIndex), AggregateType.S32)}]";
}
return $"textures.samp_{name}";
return $"{setName}.samp_{name}";
}
private static string GetImageName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex)
{
TextureDefinition definition = context.Properties.Images[texOp.GetTextureSetAndBinding()];
string name = definition.Name;
TextureDefinition imageDefinition = context.Properties.Images[texOp.GetTextureSetAndBinding()];
string name = imageDefinition.Name;
string setName = Declarations.GetNameForSet(imageDefinition.Set, true);
if (definition.ArrayLength != 1)
if (imageDefinition.ArrayLength != 1)
{
name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]";
}
return $"images.{name}";
return $"{setName}.{name}";
}
private static string GetMaskMultiDest(int mask)

View file

@ -20,28 +20,28 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
CodeGenContext context = new(info, parameters);
Declarations.Declare(context, info);
var sets = Declarations.Declare(context, info);
if (info.Functions.Count != 0)
{
for (int i = 1; i < info.Functions.Count; i++)
{
PrintFunction(context, info.Functions[i], parameters.Definitions.Stage);
PrintFunction(context, info.Functions[i], parameters.Definitions.Stage, sets);
context.AppendLine();
}
}
PrintFunction(context, info.Functions[0], parameters.Definitions.Stage, true);
PrintFunction(context, info.Functions[0], parameters.Definitions.Stage, sets, true);
return context.GetCode();
}
private static void PrintFunction(CodeGenContext context, StructuredFunction function, ShaderStage stage, bool isMainFunc = false)
private static void PrintFunction(CodeGenContext context, StructuredFunction function, ShaderStage stage, int[] sets, bool isMainFunc = false)
{
context.CurrentFunction = function;
context.AppendLine(GetFunctionSignature(context, function, stage, isMainFunc));
context.AppendLine(GetFunctionSignature(context, function, stage, sets, isMainFunc));
context.EnterScope();
Declarations.DeclareLocals(context, function, stage, isMainFunc);
@ -61,6 +61,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
CodeGenContext context,
StructuredFunction function,
ShaderStage stage,
int[] sets,
bool isMainFunc = false)
{
int additionalArgCount = isMainFunc ? 0 : CodeGenContext.AdditionalArgCount + (context.Definitions.Stage != ShaderStage.Compute ? 1 : 0);
@ -166,8 +167,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl
args = args.Append($"constant ConstantBuffers &constant_buffers [[buffer({Defaults.ConstantBuffersIndex})]]").ToArray();
args = args.Append($"device StorageBuffers &storage_buffers [[buffer({Defaults.StorageBuffersIndex})]]").ToArray();
args = args.Append($"constant Textures &textures [[buffer({Defaults.TexturesIndex})]]").ToArray();
args = args.Append($"constant Images &images [[buffer({Defaults.ImagesIndex})]]").ToArray();
foreach (var set in sets)
{
var bindingIndex = set + Defaults.BaseSetIndex;
args = args.Append($"constant {Declarations.GetNameForSet(set)} &{Declarations.GetNameForSet(set, true)} [[buffer({bindingIndex})]]").ToArray();
}
}
var funcPrefix = $"{funcKeyword} {returnType} {funcName ?? function.Name}(";

View file

@ -192,7 +192,7 @@ namespace Ryujinx.Graphics.Shader
typeName += "_array";
}
return $"{typeName} <float{(image ? ", access::read_write" : "")}>";
return $"{typeName}<float{(image ? ", access::read_write" : "")}>";
}
}
}