From 77d5881e2591751ce0133fb347171f1eb0917b04 Mon Sep 17 00:00:00 2001 From: kuba Date: Sun, 27 Sep 2026 19:52:09 +0200 Subject: [PATCH 1/2] [SpirvTranslator] Fill the FRONT_FACE and ANCILLARY pixel inputs A pixel shader that enables FRONT_FACE or ANCILLARY reads those VGPRs, but the translator only reserved their compact slots and left them reading zero. Unreal Engine bakes its colour grading LUT one volume slice per instance and has the pixel shader read the slice back out of ANCILLARY[28:16], the render target array index. With that register zero every slice was baked from the blue = 0 slice, so the tonemapped image lost its blue channel. ANCILLARY is now the Layer built-in placed at bit 16, and FRONT_FACE the FrontFacing built-in mapped to 1.0f for a front face and 0 for a back face, which satisfies both the float greater-than and the integer not-equal tests compilers emit for it. --- .../Gen5SpirvTranslator.Interpolation.cs | 53 +++++++++++++++++++ .../Gen5SpirvTranslator.cs | 9 ++-- .../Gen5InterpolationParameterTests.cs | 16 ++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs index 7700cbf..478f501 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs @@ -13,6 +13,8 @@ public static partial class Gen5SpirvTranslator private readonly HashSet _flatParameterAttributes = []; private readonly Dictionary _barycentricInputs = []; private uint _interpolationSampleId; + private uint _frontFacingInput; + private uint _ancillaryLayerInput; private const uint InterpolateAtCentroid = 76; private const uint InterpolateAtSample = 77; private const uint InterpolateAtOffset = 78; @@ -90,6 +92,57 @@ 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); + } + } + + // 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 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 4c4a7b9..fe315d2 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -942,6 +942,7 @@ public static partial class Gen5SpirvTranslator SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.FragCoord); _interfaces.Add(_fragCoordInput); + DeclarePixelSystemInputs(); var declaredPixelOutputs = Environment.GetEnvironmentVariable( @@ -1217,10 +1218,10 @@ 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); + // position inputs. SAMPLE_COVERAGE and POS_FIXED_PT only reserve + // their compact slots until a guest shader needs them. + EmitPixelSystemInput(12, _frontFacingInput == 0 ? 0 : LoadFrontFaceInput(), ref vgpr); + EmitPixelSystemInput(13, _ancillaryLayerInput == 0 ? 0 : LoadAncillaryInput(), ref vgpr); AdvancePixelInput(14, 1, ref vgpr); AdvancePixelInput(15, 1, ref vgpr); } diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs index d07b6f3..06cbb21 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -107,6 +107,22 @@ public sealed class Gen5InterpolationParameterTests instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr); } + [Fact] + public void FrontFaceAndAncillary_ReadTheirBuiltIns() + { + Assert.True(Gen5SpirvTranslator.TryCompileProgram( + Request(0, false, inputs: 0x3002, 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(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) From 223a1b78731a9dfa667861161324f764a91fcebc Mon Sep 17 00:00:00 2001 From: kuba Date: Tue, 29 Sep 2026 23:15:26 +0200 Subject: [PATCH 2/2] [SpirvTranslator] Fill the SAMPLE_COVERAGE and POS_FIXED_PT pixel inputs The two remaining reserved pixel system inputs were also left reading zero. SAMPLE_COVERAGE now reads the first element of the SampleMask built-in, and POS_FIXED_PT packs the integer pixel position taken from FragCoord, x in bits [15:0] and y in bits [31:16], so no enabled pixel system input is left unset. --- .../Gen5SpirvTranslator.Interpolation.cs | 30 +++++++++++++++++++ .../Gen5SpirvTranslator.cs | 7 ++--- .../SpirvModuleBuilder.cs | 1 + .../Gen5InterpolationParameterTests.cs | 5 ++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs index 478f501..48d5df0 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs @@ -15,6 +15,7 @@ public static partial class Gen5SpirvTranslator 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; @@ -117,6 +118,16 @@ public static partial class Gen5SpirvTranslator _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 @@ -127,6 +138,25 @@ public static partial class Gen5SpirvTranslator 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; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index fe315d2..0a77df0 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1218,12 +1218,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. SAMPLE_COVERAGE and POS_FIXED_PT only reserve - // their compact slots until a guest shader needs them. + // position inputs. EmitPixelSystemInput(12, _frontFacingInput == 0 ? 0 : LoadFrontFaceInput(), ref vgpr); EmitPixelSystemInput(13, _ancillaryLayerInput == 0 ? 0 : LoadAncillaryInput(), ref vgpr); - AdvancePixelInput(14, 1, ref vgpr); - AdvancePixelInput(15, 1, 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 485a002..8538ba9 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 06cbb21..603ed80 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -108,10 +108,10 @@ public sealed class Gen5InterpolationParameterTests } [Fact] - public void FrontFaceAndAncillary_ReadTheirBuiltIns() + public void PixelSystemInputs_ReadTheirBuiltIns() { Assert.True(Gen5SpirvTranslator.TryCompileProgram( - Request(0, false, inputs: 0x3002, opcode: "VInterpP2F32"), out var shader, out var error), error); + 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 && @@ -119,6 +119,7 @@ public sealed class Gen5InterpolationParameterTests .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); }