diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs index 7700cbf..48d5df0 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs @@ -13,6 +13,9 @@ public static partial class Gen5SpirvTranslator private readonly HashSet _flatParameterAttributes = []; private readonly Dictionary _barycentricInputs = []; private uint _interpolationSampleId; + private uint _frontFacingInput; + private uint _ancillaryLayerInput; + private uint _sampleMaskInput; private const uint InterpolateAtCentroid = 76; private const uint InterpolateAtSample = 77; private const uint InterpolateAtOffset = 78; @@ -90,6 +93,86 @@ public static partial class Gen5SpirvTranslator } } + // FRONT_FACE and ANCILLARY are VGPRs the hardware fills for pixel shaders that enable them. + // UE writes volume textures one slice per instance and has the pixel shader read the slice + // back out of ANCILLARY[28:16] (the render target array index); leaving that register zero + // bakes every slice of a color grading LUT from the blue=0 slice. + private void DeclarePixelSystemInputs() + { + var enabledInputs = _pixelInputAddress & _pixelInputEnable; + if ((enabledInputs & (1u << 12)) != 0) + { + _frontFacingInput = _module.AddGlobalVariable( + _module.TypePointer(SpirvStorageClass.Input, _boolType), SpirvStorageClass.Input); + _module.AddDecoration(_frontFacingInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.FrontFacing); + _interfaces.Add(_frontFacingInput); + } + + if ((enabledInputs & (1u << 13)) != 0) + { + _module.AddExtension("SPV_EXT_shader_viewport_index_layer"); + _module.AddCapability(SpirvCapability.ShaderViewportIndexLayerExt); + _ancillaryLayerInput = _module.AddGlobalVariable( + _module.TypePointer(SpirvStorageClass.Input, _uintType), SpirvStorageClass.Input); + _module.AddDecoration(_ancillaryLayerInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Layer); + _module.AddDecoration(_ancillaryLayerInput, SpirvDecoration.Flat); + _interfaces.Add(_ancillaryLayerInput); + } + + if ((enabledInputs & (1u << 14)) != 0) + { + var maskArray = _module.TypeArray(_intType, 1); + _sampleMaskInput = _module.AddGlobalVariable( + _module.TypePointer(SpirvStorageClass.Input, maskArray), SpirvStorageClass.Input); + _module.AddDecoration(_sampleMaskInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.SampleMask); + _module.AddDecoration(_sampleMaskInput, SpirvDecoration.Flat); + _interfaces.Add(_sampleMaskInput); + } + } + + // Front facing reads as 1.0f and back facing as 0, which satisfies both the float greater-than + // and the integer not-equal tests compilers emit for it. + private uint LoadFrontFaceInput() => _module.AddInstruction( + SpirvOp.Select, _uintType, Load(_boolType, _frontFacingInput), UInt(0x3F800000u), UInt(0)); + + private uint LoadAncillaryInput() => + ShiftLeftLogical(Load(_uintType, _ancillaryLayerInput), UInt(16)); + + private uint LoadSampleCoverageInput() + { + var element = _module.AddInstruction(SpirvOp.AccessChain, + _module.TypePointer(SpirvStorageClass.Input, _intType), _sampleMaskInput, UInt(0)); + return Bitcast(_uintType, Load(_intType, element)); + } + + // POS_FIXED_PT holds the integer pixel position: x in bits [15:0] and y in bits [31:16]. + private uint LoadFixedPointPositionInput(uint fragCoord) + { + var x = _module.AddInstruction(SpirvOp.ConvertFToU, _uintType, + _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, fragCoord, 0)); + var y = _module.AddInstruction(SpirvOp.ConvertFToU, _uintType, + _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, fragCoord, 1)); + return _module.AddInstruction(SpirvOp.BitwiseOr, _uintType, + _module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, x, UInt(0xFFFF)), + ShiftLeftLogical(y, UInt(16))); + } + + private void EmitPixelSystemInput(int bit, uint value, ref uint vgpr) + { + var mask = 1u << bit; + if ((_pixelInputAddress & mask) == 0) + { + return; + } + + if ((_pixelInputEnable & mask) != 0 && value != 0) + { + StoreV(vgpr, value, guardWithExec: false); + } + + vgpr++; + } + private uint LoadBarycentricCoordinates(int bit, uint variable) => bit switch { 0 or 4 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtSample, diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index ee462d1..2bdfa64 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -962,6 +962,7 @@ public static partial class Gen5SpirvTranslator SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.FragCoord); _interfaces.Add(_fragCoordInput); + DeclarePixelSystemInputs(); var declaredPixelOutputs = Environment.GetEnvironmentVariable( @@ -1237,12 +1238,11 @@ public static partial class Gen5SpirvTranslator EmitPixelPositionInput(11, 3, fragCoord, ref vgpr); // POS_W_FLOAT // FRONT_FACE, ANCILLARY, SAMPLE_COVERAGE and POS_FIXED_PT follow - // position inputs. Reserve their compact slots until their SPIR-V - // builtins are needed by a guest shader. - AdvancePixelInput(12, 1, ref vgpr); - AdvancePixelInput(13, 1, ref vgpr); - AdvancePixelInput(14, 1, ref vgpr); - AdvancePixelInput(15, 1, ref vgpr); + // position inputs. + EmitPixelSystemInput(12, _frontFacingInput == 0 ? 0 : LoadFrontFaceInput(), ref vgpr); + EmitPixelSystemInput(13, _ancillaryLayerInput == 0 ? 0 : LoadAncillaryInput(), ref vgpr); + EmitPixelSystemInput(14, _sampleMaskInput == 0 ? 0 : LoadSampleCoverageInput(), ref vgpr); + EmitPixelSystemInput(15, (_pixelInputAddress & _pixelInputEnable & 0x8000u) == 0 ? 0 : LoadFixedPointPositionInput(fragCoord), ref vgpr); } private void AdvancePixelInput(int bit, uint dwordCount, ref uint vgpr) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs index 028580c..c3c240a 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs @@ -281,6 +281,7 @@ public enum SpirvBuiltIn : uint SubgroupSize = 36, SubgroupLocalInvocationId = 41, SampleId = 18, + SampleMask = 20, FragDepth = 22, } diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs index d07b6f3..603ed80 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -107,6 +107,23 @@ public sealed class Gen5InterpolationParameterTests instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr); } + [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); + } + private static ShaderCompileRequest Request( uint selector, bool custom, uint inputs = 2, string opcode = "VInterpMovF32", uint inputCntl = 0x401, bool supportsPerVertex = true)