[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.
This commit is contained in:
kuba 2026-09-26 13:19:57 +02:00 committed by GitHub
parent 155add2c10
commit 9073974ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 65 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ public static partial class Gen5SpirvTranslator
private sealed partial class CompilationContext
{
private readonly HashSet<uint> _perVertexAttributes = [];
private readonly HashSet<uint> _flatParameterAttributes = [];
private readonly Dictionary<int, uint> _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;

View File

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

View File

@ -138,6 +138,10 @@ public sealed class ShaderCompileRequest
public IReadOnlyList<Gen5PixelOutputBinding> 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<uint>? PixelInputCntl { get; init; }

View File

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