// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Diagnostics; using SharpEmu.ShaderCompiler.Resources; using SharpEmu.ShaderCompiler.Tests.Resources; using SharpEmu.ShaderCompiler.Vulkan; using Xunit; namespace SharpEmu.ShaderCompiler.Tests; public sealed class Gen5InterpolationParameterTests { [Theory] [InlineData(0u, false, 1u, true)] [InlineData(1u, false, 2u, true)] [InlineData(2u, false, 0u, false)] [InlineData(0u, true, 1u, false)] [InlineData(1u, true, 2u, false)] [InlineData(2u, true, 0u, false)] public void ParameterMove_SelectsVertexAndPreservesCustomData( uint selector, bool custom, uint vertex, bool subtractOrigin) { var request = Request(selector, custom); Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); var instructions = Instructions(shader.Spirv); Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Capability && instruction.Operands[0] == (uint)SpirvCapability.FragmentBarycentricKhr); var input = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr).Operands[0]; Assert.DoesNotContain(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[0] == input && instruction.Operands[1] == (uint)SpirvDecoration.Flat); var firstAccess = instructions.First(instruction => instruction.Opcode == SpirvOp.AccessChain && instruction.Operands[2] == input); Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Constant && instruction.Operands[1] == firstAccess.Operands[3] && instruction.Operands[2] == vertex); Assert.Equal(subtractOrigin, instructions.Any(instruction => instruction.Opcode == SpirvOp.FSub)); ValidateWhenAvailable(shader.Spirv); } [Theory] [InlineData(3u)] [InlineData(255u)] public void ReservedSelector_Fails(uint selector) { Assert.False(Gen5SpirvTranslator.TryCompileProgram(Request(selector, false), out _, out var error)); Assert.Contains("reserved interpolation parameter selector", error); } [Fact] public void MixedBarycentricLocations_ShareBuiltIns() { Assert.True(Gen5SpirvTranslator.TryCompileProgram(Request(2, true, 0x77), out var shader, out var error), error); var builtIns = Instructions(shader.Spirv) .Where(instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn) .Select(instruction => instruction.Operands[2]).ToArray(); Assert.Equal(builtIns.Length, builtIns.Distinct().Count()); Assert.Contains((uint)SpirvBuiltIn.BaryCoordKhr, builtIns); Assert.Contains((uint)SpirvBuiltIn.BaryCoordNoPerspKhr, builtIns); Assert.Contains((uint)SpirvBuiltIn.SampleId, builtIns); ValidateWhenAvailable(shader.Spirv); } [Fact] public void OrdinaryInterpolation_DoesNotRequireBarycentricFeature() { Assert.True(Gen5SpirvTranslator.TryCompileProgram( Request(0, false, opcode: "VInterpP2F32"), out var shader, out var error), error); Assert.DoesNotContain(Instructions(shader.Spirv), instruction => instruction.Opcode == SpirvOp.Capability && instruction.Operands[0] == (uint)SpirvCapability.FragmentBarycentricKhr); ValidateWhenAvailable(shader.Spirv); } [Fact] public void PullModel_DoesNotSilentlyUseZeroCoordinates() { Assert.False(Gen5SpirvTranslator.TryCompileProgram(Request(2, true, 8), out _, out var error)); 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_FallsBackToInterpolatedInputWithoutPerVertexSupport(uint selector) { var request = Request(selector, 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); Assert.DoesNotContain(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.Flat); ValidateWhenAvailable(shader.Spirv); } [Fact] public void PixelSystemInputs_ReadTheirBuiltIns() { Assert.True(Gen5SpirvTranslator.TryCompileProgram( Request(0, false, inputs: 0xF002, opcode: "VInterpP2F32"), out var shader, out var error), error); var instructions = Instructions(shader.Spirv); var builtIns = instructions .Where(instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn) .Select(instruction => instruction.Operands[2]).ToArray(); Assert.Contains((uint)SpirvBuiltIn.FrontFacing, builtIns); Assert.Contains((uint)SpirvBuiltIn.Layer, builtIns); Assert.Contains((uint)SpirvBuiltIn.SampleMask, builtIns); Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.ShiftLeftLogical); ValidateWhenAvailable(shader.Spirv); } [Fact] public void SlotsReadingOneParameter_ShareOneInput() { // PS slots 1 and 2 both read VS parameter 1; the second slot must not move to a // location the vertex program never writes. Gen5ShaderInstruction Move(uint pc, uint attribute, uint destination) => new(pc, Gen5ShaderEncoding.Vintrp, "VInterpMovF32", [1], [Gen5Operand.Vector(1)], [Gen5Operand.Vector(destination)], new Gen5InterpolationControl(attribute, 0)); var program = ResourceTestProgram.Program(Move(0, 1, 4), Move(4, 2, 5), ResourceTestProgram.EndProgram(8)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); var request = new ShaderCompileRequest(plan, resources, layout) { PixelInputAddress = 2, PixelInputEnable = 2, PixelInputCntl = [0, 0x1, 0x1], PixelCustomInterpolationMask = 6, SupportsPerVertexPixelInputs = true, }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); var instructions = Instructions(shader.Spirv); var input = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr).Operands[0]; var location = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[0] == input && instruction.Operands[1] == (uint)SpirvDecoration.Location); Assert.Equal(1u, location.Operands[2]); Assert.Equal(2, instructions.Count(instruction => instruction.Opcode == SpirvOp.AccessChain && instruction.Operands[2] == input)); ValidateWhenAvailable(shader.Spirv); } [Fact] public void SmoothSlotSharingAPerVertexParameter_InterpolatesThePerVertexInput() { // Slot 1 is read per vertex, slot 2 interpolates the same VS parameter: one per-vertex // input serves both, and slot 2 is rebuilt from the vertices with the barycentrics. var move = new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Vintrp, "VInterpMovF32", [1], [Gen5Operand.Vector(1)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(1, 0)); var smooth = new Gen5ShaderInstruction(4, Gen5ShaderEncoding.Vintrp, "VInterpP2F32", [0], [Gen5Operand.Vector(1)], [Gen5Operand.Vector(5)], new Gen5InterpolationControl(2, 0)); var program = ResourceTestProgram.Program(move, smooth, ResourceTestProgram.EndProgram(8)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); var request = new ShaderCompileRequest(plan, resources, layout) { PixelInputAddress = 2, PixelInputEnable = 2, PixelInputCntl = [0, 0x1, 0x1], PixelCustomInterpolationMask = 2, SupportsPerVertexPixelInputs = true, }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); var instructions = Instructions(shader.Spirv); var input = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr).Operands[0]; var inputLocations = instructions.Where(instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.Location && instruction.Operands[0] == input).ToArray(); Assert.Equal(1u, Assert.Single(inputLocations).Operands[2]); Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn && instruction.Operands[2] == (uint)SpirvBuiltIn.BaryCoordKhr); Assert.Equal(4, instructions.Count(instruction => instruction.Opcode == SpirvOp.AccessChain && instruction.Operands[2] == input)); ValidateWhenAvailable(shader.Spirv); } [Theory] [InlineData(0x800u, 1)] [InlineData(0x400u, 0)] public void PositionW_IsTheReciprocalOfTheFragmentCoordinate(uint inputs, int reciprocals) { var program = ResourceTestProgram.Program(ResourceTestProgram.EndProgram(0)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); var request = new ShaderCompileRequest(plan, resources, layout) { PixelInputAddress = inputs, PixelInputEnable = inputs }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); var instructions = Instructions(shader.Spirv); var position = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn && instruction.Operands[2] == (uint)SpirvBuiltIn.FragCoord).Operands[0]; var loaded = instructions.Where(instruction => instruction.Opcode == SpirvOp.Load && instruction.Operands[2] == position) .Select(instruction => instruction.Operands[1]).ToHashSet(); var w = instructions.Where(instruction => instruction.Opcode == SpirvOp.CompositeExtract && loaded.Contains(instruction.Operands[2]) && instruction.Operands[3] == 3).Select(instruction => instruction.Operands[1]).ToHashSet(); Assert.Equal(reciprocals, instructions.Count(instruction => instruction.Opcode == SpirvOp.FDiv && w.Contains(instruction.Operands[3]))); ValidateWhenAvailable(shader.Spirv); } [Theory] [InlineData(ShaderStage.Pixel, 32u)] [InlineData(ShaderStage.Pixel, 64u)] [InlineData(ShaderStage.Vertex, 32u)] [InlineData(ShaderStage.Vertex, 64u)] public void LaneSpills_AreReadBackWithoutTheHostSubgroup(ShaderStage stage, uint waveSize) { var program = ResourceTestProgram.Program( ResourceTestProgram.WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 5), ResourceTestProgram.WriteLane(8, vectorRegister: 18, scalarRegister: 85, lane: 37), ResourceTestProgram.ReadLane(16, scalarRegister: 86, vectorRegister: 18, lane: 5), ResourceTestProgram.ReadLane(24, scalarRegister: 87, vectorRegister: 18, lane: 37), ResourceTestProgram.EndProgram(32)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, stage, userDataCount: 0); var request = new ShaderCompileRequest(plan, resources, layout) { WaveSize = waveSize, EnableGraphicsSubgroupOperations = true }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); Assert.DoesNotContain(Instructions(shader.Spirv), instruction => instruction.Opcode == SpirvOp.GroupNonUniformBroadcast); var text = System.Text.Encoding.ASCII.GetString(shader.Spirv); Assert.Contains("v18_lane5", text, StringComparison.Ordinal); Assert.Contains("v18_lane37", text, StringComparison.Ordinal); ValidateWhenAvailable(shader.Spirv); } [Theory] [InlineData(ShaderStage.Pixel, 0)] [InlineData(ShaderStage.Vertex, 0)] [InlineData(ShaderStage.Compute, 2)] public void ReadlaneOfAnUnspilledLane_UsesTheHostSubgroupOnlyInCompute(ShaderStage stage, int broadcasts) { var program = ResourceTestProgram.Program( ResourceTestProgram.WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 5), ResourceTestProgram.ReadLane(8, scalarRegister: 86, vectorRegister: 18, lane: 6), ResourceTestProgram.ReadLane(16, scalarRegister: 87, vectorRegister: 19, lane: 5), ResourceTestProgram.EndProgram(24)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, stage, userDataCount: 0); var request = new ShaderCompileRequest(plan, resources, layout) { WaveSize = 32, EnableGraphicsSubgroupOperations = true }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); Assert.Equal(broadcasts, Instructions(shader.Spirv).Count(instruction => instruction.Opcode == SpirvOp.GroupNonUniformBroadcast)); ValidateWhenAvailable(shader.Spirv); } private static ShaderCompileRequest Request( 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)); var program = ResourceTestProgram.Program(interpolation, ResourceTestProgram.EndProgram(4)); var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); return new ShaderCompileRequest(plan, resources, layout) { PixelInputAddress = inputs, PixelInputEnable = inputs, PixelInputCntl = [0, inputCntl], PixelCustomInterpolationMask = custom ? 2u : 0u, SupportsPerVertexPixelInputs = supportsPerVertex, }; } private static List Instructions(byte[] code) { var words = new uint[code.Length / 4]; Buffer.BlockCopy(code, 0, words, 0, code.Length); var result = new List(); for (var index = 5; index < words.Length;) { var count = (int)(words[index] >> 16); result.Add(new Instruction((SpirvOp)(words[index] & 0xFFFF), words[(index + 1)..(index + count)])); index += count; } return result; } private static void ValidateWhenAvailable(byte[] code) { var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); if (string.IsNullOrWhiteSpace(sdk)) { return; } var executable = Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val"); if (!File.Exists(executable)) { return; } var path = Path.GetTempFileName(); try { File.WriteAllBytes(path, code); var start = new ProcessStartInfo(executable) { UseShellExecute = false, RedirectStandardError = true, CreateNoWindow = true, }; start.ArgumentList.Add("--target-env"); start.ArgumentList.Add("vulkan1.2"); start.ArgumentList.Add(path); using var process = Process.Start(start)!; var error = process.StandardError.ReadToEnd(); process.WaitForExit(); Assert.True(process.ExitCode == 0, error); } finally { File.Delete(path); } } private sealed record Instruction(SpirvOp Opcode, uint[] Operands); }