From 9073974ba8b916a9809f8d40f19880fb031725f3 Mon Sep 17 00:00:00 2001 From: kuba Date: Sat, 26 Sep 2026 13:19:57 +0200 Subject: [PATCH] [SpirvTranslator] Use a flat input for v_interp_mov P0 on MoltenVK (#910) MoltenVK advertises VK_KHR_fragment_shader_barycentric, but SPIRV-Cross cannot translate PerVertexKHR inputs to MSL, so every pixel shader with v_interp_mov_f32 failed pipeline creation on macOS (Poppy Playtime). Reading only P0 is the provoking vertex value, which a flat input gives. Attributes that read P10 or P20 still use per-vertex inputs. --- .../Gpu/Pipelines/IShaderPipelineHost.cs | 2 ++ .../Gpu/Pipelines/ShaderProgramCache.cs | 1 + .../VulkanVideoPresenter.Device.Setup.cs | 5 +++ .../VulkanVideoPresenter.Pipelines.cs | 2 ++ .../Gen5SpirvTranslator.Interpolation.cs | 18 ++++++++++ .../Gen5SpirvTranslator.cs | 2 +- .../ShaderCompileRequest.cs | 4 +++ .../Gen5InterpolationParameterTests.cs | 34 +++++++++++++++++-- 8 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs index b241d82..aac1816 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs @@ -38,6 +38,8 @@ internal interface IShaderPipelineHost bool GraphicsSubgroupOperationsEnabled { get; } + bool PerVertexPixelInputsSupported => true; + RenderHostLimits Limits { get; } // The sample counts a pipeline without attachments can rasterize at. diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index 98fd6af..ef29e5f 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -560,6 +560,7 @@ internal sealed class ShaderProgramCache PixelOutputs = options.PixelOutputs, PixelInputEnable = options.PixelInputEnable, PixelCustomInterpolationMask = info.CustomInterpolationMask, + SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported, PixelInputAddress = options.PixelInputAddress, PixelInputCntl = interpolators, }; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index aaa97d7..e84876f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -645,6 +645,7 @@ internal static unsafe partial class VulkanVideoPresenter } private bool _supportsFragmentShaderBarycentric; + private bool _supportsPerVertexPixelInputs; private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric"; private void CreateDevice() @@ -742,6 +743,10 @@ internal static unsafe partial class VulkanVideoPresenter _supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric; } + // MoltenVK exposes the barycentric builtins, but SPIRV-Cross rejects PerVertexKHR inputs. + _supportsPerVertexPixelInputs = _supportsFragmentShaderBarycentric && + !IsDeviceExtensionAvailable(PortabilitySubsetExtensionName); + var depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT { SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt, diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index b522a5f..b286fc9 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -66,6 +66,8 @@ internal static unsafe partial class VulkanVideoPresenter bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled; + bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs; + RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits; SampleCountFlags IShaderPipelineHost.NoAttachmentSampleCounts => _noAttachmentSampleCounts; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs index d297c93..7700cbf 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs @@ -10,6 +10,7 @@ public static partial class Gen5SpirvTranslator private sealed partial class CompilationContext { private readonly HashSet _perVertexAttributes = []; + private readonly HashSet _flatParameterAttributes = []; private readonly Dictionary _barycentricInputs = []; private uint _interpolationSampleId; private const uint InterpolateAtCentroid = 76; @@ -27,6 +28,23 @@ public static partial class Gen5SpirvTranslator } } + if (!_request.SupportsPerVertexPixelInputs) + { + // Reading P0 alone is the provoking vertex value, which a flat input gives. + // Other parameters still need the per-vertex values. + foreach (var attribute in _perVertexAttributes.ToArray()) + { + if (_request.Program.Instructions.All(instruction => + instruction.Control is not Gen5InterpolationControl control || + control.Attribute != attribute || + (instruction.Opcode == "VInterpMovF32" && (instruction.Words[0] & 0xFFu) == 2))) + { + _perVertexAttributes.Remove(attribute); + _flatParameterAttributes.Add(attribute); + } + } + } + if (_perVertexAttributes.Count == 0) { return; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index ab38204..81cf6fe 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -873,7 +873,7 @@ public static partial class Gen5SpirvTranslator { _module.AddDecoration(variable, SpirvDecoration.PerVertexKhr); } - else if ((cntl & 0x400u) != 0) + else if ((cntl & 0x400u) != 0 || _flatParameterAttributes.Contains(attribute)) { _module.AddDecoration(variable, SpirvDecoration.Flat); } diff --git a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs index 3cd0391..bb80cc6 100644 --- a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs +++ b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs @@ -138,6 +138,10 @@ public sealed class ShaderCompileRequest public IReadOnlyList PixelOutputs { get; init; } = []; public uint PixelInputEnable { get; init; } public uint PixelCustomInterpolationMask { get; init; } + + // False when the device cannot read one vertex's value of a pixel input (PerVertexKHR). + // MoltenVK advertises fragment shader barycentrics but cannot translate PerVertexKHR to MSL. + public bool SupportsPerVertexPixelInputs { get; init; } = true; public uint PixelInputAddress { get; init; } public IReadOnlyList? PixelInputCntl { get; init; } diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs index 06845a9..d07b6f3 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -79,8 +79,37 @@ public sealed class Gen5InterpolationParameterTests Assert.Contains("Pull-model interpolation", error); } + [Fact] + public void ProvokingVertexMove_UsesAFlatInputWithoutPerVertexSupport() + { + var request = Request(2, false, inputCntl: 0x1, supportsPerVertex: false); + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + var instructions = Instructions(shader.Spirv); + Assert.DoesNotContain(instructions, instruction => instruction.Opcode == SpirvOp.Capability && + instruction.Operands[0] == (uint)SpirvCapability.FragmentBarycentricKhr); + Assert.DoesNotContain(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr); + var input = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.Location).Operands[0]; + Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[0] == input && instruction.Operands[1] == (uint)SpirvDecoration.Flat); + ValidateWhenAvailable(shader.Spirv); + } + + [Theory] + [InlineData(0u)] + [InlineData(1u)] + public void VertexDifferenceMove_KeepsPerVertexInputWithoutPerVertexSupport(uint selector) + { + var request = Request(selector, false, inputCntl: 0x1, supportsPerVertex: false); + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + Assert.Contains(Instructions(shader.Spirv), instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr); + } + private static ShaderCompileRequest Request( - uint selector, bool custom, uint inputs = 2, string opcode = "VInterpMovF32") + uint selector, bool custom, uint inputs = 2, string opcode = "VInterpMovF32", + uint inputCntl = 0x401, bool supportsPerVertex = true) { var interpolation = new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Vintrp, opcode, [selector], [Gen5Operand.Vector(selector)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(1, 2)); @@ -90,8 +119,9 @@ public sealed class Gen5InterpolationParameterTests { PixelInputAddress = inputs, PixelInputEnable = inputs, - PixelInputCntl = [0, 0x401], + PixelInputCntl = [0, inputCntl], PixelCustomInterpolationMask = custom ? 2u : 0u, + SupportsPerVertexPixelInputs = supportsPerVertex, }; }