From daa6bee7cf954bbc122a24054d9c7ab2cdc58af5 Mon Sep 17 00:00:00 2001 From: Daan Dehing Date: Mon, 5 Oct 2026 16:24:25 +0000 Subject: [PATCH] feat(shader): implement v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16 Decode the three VOP3 16-bit interpolation instructions (attribute, channel and high-half select in the src0 field, the I/J VGPR in src1, P0 or the first step's result in src2) and translate them with a new GetInterpolationParameterF16 IR value. It reads the selected 16-bit half of each vertex's parameter through VK_KHR_fragment_shader_barycentric, so P10 and P20 are vertex differences rounded to f16; both steps are a multiply and an add, as in the f32 forms. An input in FP16_INTERP_MODE whose low half is defaulted is still declared at its slot when its high half is read, and the high half takes its default from USE_DEFAULT_ATTR1 and DEFAULT_VAL_ATTR1. Without barycentrics, on an input without FP16_INTERP_MODE or on a pass-through input the instructions throw. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 14 + .../tests/execution/InterpolationF16.cpp | 295 ++++++++++++++++++ .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 1 + .../Optimization/ShaderStageInputInfo.hpp | 20 ++ .../Optimization/src/ShaderInfoCollector.cpp | 27 +- .../include/RdnaDecoder/RdnaOpcode.hpp | 3 + .../recompiler/RdnaDecoder/src/RdnaOpcode.cpp | 3 + .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 45 +++ .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvAnalysis.cpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../src/SpirvMemory/SpirvInputOutput.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 36 +++ .../Translation/TranslationContext.hpp | 3 + .../Translation/src/AttributeInstructions.cpp | 35 ++- .../Translation/src/DispatchInstructions.cpp | 2 +- docs/dev/TechnicalDebt.md | 1 + 18 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 707e06f2c..013df8348 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1172,6 +1172,19 @@ void verifyPixelParameterSlots() { expectFailure([] { static_cast(recompileSlots({0x423u, 0x3u}, shared)); }, "passes its vertices through unchanged", "an interpolated pass-through input was accepted"); } +void verifyF16PixelParameterSlots() { + static constexpr std::array high{0xd7420002u, 0x00020100u, 0xd75a0003u, 0x040a0300u, 0xf800180fu, 0x03030303u, 0xbf810000u}; + static constexpr std::array low{0xd7420002u, 0x00020000u, 0xd75a0003u, 0x040a0200u, 0xf800180fu, 0x03030303u, 0xbf810000u}; + auto inputs = slotInputs({0x03080003u}, high); + require(inputs.size() == 1u && inputs[0].first == 3u && inputs[0].second, "a 16-bit interpolated input was not read per vertex at its slot"); + inputs = slotInputs({0x03080023u}, high); + require(inputs.size() == 1u && inputs[0].first == 3u && inputs[0].second, "an input with a defaulted low half was not read per vertex for its high half"); + require(slotInputs({0x03180023u}, high).empty(), "an input whose high half is defaulted was declared for a high-half read"); + require(slotInputs({0x03080023u}, low).empty(), "an input whose low half is defaulted was declared for a low-half read"); + inputs = slotInputs({0x03180003u}, low); + require(inputs.size() == 1u && inputs[0].first == 3u && inputs[0].second, "an input with a defaulted high half was not read per vertex for its low half"); +} + } void verifyComputedTexelOffsets() { @@ -1769,6 +1782,7 @@ int main(int argc, char** argv) { verifyLegacyPixelRequests(); verifyPixelExportReplay(); verifyPixelParameterSlots(); + verifyF16PixelParameterSlots(); verifyComputedTexelOffsets(); verifyShaderClockScopes(); verifyUnnormalizedSamplers(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp new file mode 100644 index 000000000..d69c07f7f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -0,0 +1,295 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ResultDwords = 12; +constexpr std::uint32_t Fp16Input = 0x03080000u; +constexpr std::uint32_t Fp16DefaultHighInput = 0x01580000u; +alignas(256) std::array Pixels{}; +alignas(256) std::array Results{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000u, 0x80000005u, 0xe0382010u, 0x80000605u, 0xbf8c3f70u, 0xf800020fu, 0x09080706u, 0xf80008cfu, 0x03020100u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e080f02u, 0x7e0a0f03u, 0xd5430006u, 0x0411ff05u, 0x000000c0u, 0x7e300300u, 0x7e320301u, 0xd742001au, + 0x00020000u, 0x7e3602ffu, 0xabcd0000u, 0xd75a001bu, 0x046a0200u, 0xd7420008u, 0x00020100u, 0x7e3802ffu, + 0x12340000u, 0xd75a001cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, + 0xd75a001eu, 0x04760340u, 0x7e3e0280u, 0xd75a801fu, 0x046a0200u, 0xd7420020u, 0x00020181u, 0xd7420021u, + 0x000201c1u, 0xd7420022u, 0x40020000u, 0xd7430023u, 0x043e0000u, 0xe0782000u, 0x80011806u, 0xe0782010u, + 0x80011c06u, 0xe0782020u, 0x80012006u, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, +}; + +constexpr std::array LowX{0x3400u, 0x3f00u, 0xb800u}; +constexpr std::array HighX{0x4200u, 0xc100u, 0x4700u}; +constexpr std::array LowY{0x3800u, 0x3a00u, 0x3000u}; +constexpr std::array HighY{0x6c00u, 0x3c00u, 0xc800u}; + +struct Vertex { + std::array position; + std::array parameter; +}; + +float HalfToFloat(std::uint32_t half) { + const auto sign = (half & 0x8000u) != 0u ? -1.0f : 1.0f; + const auto exponent = static_cast((half >> 10u) & 0x1fu); + const auto mantissa = static_cast(half & 0x3ffu); + if (exponent == 0) return sign * std::ldexp(mantissa, -24); + if (exponent == 31) return mantissa == 0.0f ? sign * INFINITY : NAN; + return sign * std::ldexp(1024.0f + mantissa, exponent - 25); +} + +std::uint32_t FloatToHalf(float value) { + const auto bits = std::bit_cast(value); + const auto sign = (bits >> 16u) & 0x8000u; + const auto magnitude = bits & 0x7fffffffu; + if (magnitude > 0x7f800000u) return sign | 0x7e00u; + if (magnitude >= 0x477ff000u) return sign | 0x7c00u; + if (magnitude < 0x38800000u) { + const auto scaled = std::fabs(value) * 16777216.0f; + return sign | static_cast(std::nearbyint(scaled)); + } + const auto rounded = magnitude + 0xfffu + ((magnitude >> 13u) & 1u); + return sign | ((rounded - 0x38000000u) >> 13u); +} + +float RoundToHalf(float value) { + return HalfToFloat(FloatToHalf(value)); +} + +std::uint32_t Pack(std::uint32_t low, std::uint32_t high) { + return low | (high << 16u); +} + +std::vector Triangles() { + std::vector vertices; + constexpr std::uint32_t columns = 6; + constexpr std::uint32_t rows = 4; + for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { + const float width = 2.0f / columns; + const float height = 2.0f / rows; + const float x = -1.0f + width * static_cast(cell % columns); + const float y = -1.0f + height * static_cast(cell / columns); + const float skew = 0.07f * static_cast(cell % 5u); + const std::array, 3> corners{{ + {x + (0.05f + skew) * width, y + 0.05f * height}, + {x + 0.93f * width, y + (0.11f + skew) * height}, + {x + (0.37f - skew * 0.5f) * width, y + 0.91f * height}, + }}; + for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) { + vertices.push_back({{corners[vertex][0], corners[vertex][1], 0.5f, 1.0f}, {Pack(LowX[vertex], HighX[vertex]), Pack(LowY[vertex], HighY[vertex]), 0x3c003c00u, 0x3c003c00u}}); + } + } + return vertices; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelStage(std::uint32_t waveSize, std::uint32_t input0) { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.interpolatorCount = 2; + pixel.interpolatorSettings[0] = input0; + pixel.interpolatorSettings[1] = Fp16DefaultHighInput; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY); + pixel.hasPerspectiveCenterVgpr = true; + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + return pixel; +} + +ShaderRecompiler::RecompileResult RecompilePixel(std::span code, std::uint32_t waveSize, std::uint32_t input0, const ShaderRecompiler::SpirvTarget& target, std::uint32_t pushOffset) { + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), ResultDwords * 4u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, PixelStage(waveSize, input0), std::nullopt, pixelMemory}, + target, + {0, 0, pushOffset, 128 - pushOffset} + }; + fragment.useCache = false; + return ShaderRecompiler::Recompile(fragment); +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + static const auto triangles = Triangles(); + const auto target = device.Target(); + Pixels.fill(std::byte{0}); + Results.fill(Sentinel); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), sizeof(Vertex), static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelResult = RecompilePixel(PixelCode, waveSize, Fp16Input, target, vertexPush); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, vertexPush} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +float Delta(const std::array& values, std::uint32_t vertex) { + return RoundToHalf(HalfToFloat(values[vertex]) - HalfToFloat(values[0])); +} + +float FirstStep(float delta, float coordinate, float origin) { + const float product = delta * coordinate; + return product + origin; +} + +float SecondStep(float delta, float coordinate, float firstStep) { + const float product = delta * coordinate; + return product + firstStep; +} + +float Saturate(float value) { + return value > 0.0f ? std::min(value, 1.0f) : 0.0f; +} + +void Check(std::uint32_t waveSize) { + const auto what = "16-bit interpolation wave" + std::to_string(waveSize); + std::uint32_t covered = 0; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto* result = &Results[pixel * ResultDwords]; + if (std::to_integer(Pixels[pixel * 4u]) != 255u) { + Require(result[0] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored a result"); + continue; + } + ++covered; + const float i = std::bit_cast(result[0]); + const float j = std::bit_cast(result[1]); + const auto lowX = FirstStep(Delta(LowX, 1), i, HalfToFloat(LowX[0])); + const auto highX = FirstStep(Delta(HighX, 1), i, HalfToFloat(HighX[0])); + const auto highY = FirstStep(Delta(HighY, 1), i, HalfToFloat(0x4500u)); + const std::array expected{ + result[0], + result[1], + std::bit_cast(lowX), + 0xabcd0000u | FloatToHalf(SecondStep(Delta(LowX, 2), j, lowX)), + 0x12340000u | FloatToHalf(SecondStep(Delta(HighX, 2), j, highX)), + std::bit_cast(highY), + FloatToHalf(SecondStep(Delta(HighY, 2), j, highY)), + FloatToHalf(Saturate(SecondStep(Delta(LowX, 2), j, lowX))), + std::bit_cast(FirstStep(0.0f, i, 1.0f)), + std::bit_cast(FirstStep(0.0f, i, 0.0f)), + std::bit_cast(FirstStep(Delta(LowX, 1), -i, HalfToFloat(LowX[0]))), + std::bit_cast(FirstStep(Delta(LowX, 1), i, -1.0f)), + }; + for (std::uint32_t dword = 2; dword < ResultDwords; ++dword) { + char text[160]; + std::snprintf(text, sizeof(text), ": pixel %u dword %u is 0x%08x, expected 0x%08x (I %a, J %a)", pixel, dword, result[dword], expected[dword], static_cast(i), static_cast(j)); + Require(result[dword] == expected[dword], what + text); + } + } + Require(covered > 1000, what + ": the triangles covered only " + std::to_string(covered) + " pixels"); +} + +ShaderRecompiler::SpirvTarget OfflineTarget(bool barycentric) { + ShaderRecompiler::SpirvTarget target{}; + target.vulkanVersion = 0x00401000u; + target.spirvVersion = 0x00010300u; + target.subgroupSize = 64; + target.fragmentShaderBarycentricEnabled = barycentric; + return target; +} + +void ExpectFailure(std::span code, std::uint32_t input0, bool barycentric, const char* expected, const char* message) { + try { + static_cast(RecompilePixel(code, 64u, input0, OfflineTarget(barycentric), 0u)); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, std::string(message) + ": " + error.what()); + return; + } + throw std::runtime_error(message); +} + +void ExpectRejected(std::uint32_t word0, std::uint32_t word1, const char* expected, const char* message) { + alignas(256) const std::array code{word0, word1, 0xbf810000u}; + ExpectFailure(code, Fp16Input, true, expected, message); +} + +} + +int main() { + try { + ExpectFailure(PixelCode, Fp16Input, false, "requires fragmentShaderBarycentric", "16-bit interpolation was accepted without barycentrics"); + ExpectFailure(PixelCode, 0x03000000u, true, "without FP16_INTERP_MODE", "16-bit interpolation of a 32-bit input was accepted"); + ExpectFailure(PixelCode, 0x03080420u, true, "passes its vertices through unchanged", "16-bit interpolation of a pass-through input was accepted"); + ExpectRejected(0xd75a001bu, 0x0c6a0200u, "interpolation modifiers", "v_interp_p2_f16 accepted an output modifier"); + ExpectRejected(0xd75a081bu, 0x046a0200u, "interpolation modifiers", "v_interp_p2_f16 accepted op_sel"); + ExpectRejected(0xd742001au, 0x20020000u, "interpolation modifiers", "v_interp_p1ll_f16 accepted a negated attribute"); + ExpectRejected(0xd742001au, 0x00000000u, "not a vector register", "v_interp_p1ll_f16 accepted a scalar I coordinate"); + ExpectRejected(0xd743001du, 0x003e0140u, "not a vector register", "v_interp_p1lv_f16 accepted a scalar P0"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->Target().fragmentShaderBarycentricEnabled) { + std::puts("skipped, the device has no fragmentShaderBarycentric"); + return VulkanTestSkipped; + } + for (const auto waveSize : {64u, 32u}) { + Draw(*device, waveSize); + Check(waveSize); + } + std::puts("16-bit interpolation tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 484c34519..c3e1ee0d1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -457,6 +457,7 @@ enum class IrOpcode : std::uint16_t { ImageAtomicCmpSwap64, GetAttribute, GetInterpolationParameter, + GetInterpolationParameterF16, SetAttribute, ControlNop, Waitcnt, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 5919c08e6..4bc5ffcff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -498,6 +498,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("ImageAtomicCmpSwap64", U64, ImageResource, ImageAddress, U64, U64, U1), makeMeta("GetAttribute", U32, U32, U32), makeMeta("GetInterpolationParameter", U32, U32, U32, U32), + makeMeta("GetInterpolationParameterF16", F32, U32, U32, U32, U32), makeMeta("SetAttribute", Void, U32x4, U1), makeMeta("ControlNop", Void), makeMeta("Waitcnt", Void), diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 45aed32d9..996730b67 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -201,6 +201,26 @@ struct ShaderPixelInputInfo { return one ? 0x3f800000u : 0u; } + [[nodiscard]] bool InputIsFp16(std::uint32_t input) const { + return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x80000u) != 0u; + } + + [[nodiscard]] bool InputHalfIsDefault(std::uint32_t input, bool high) const { + if (!high) { + return InputIsDefault(input); + } + return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x100000u) != 0u; + } + + [[nodiscard]] std::uint32_t InputHalfDefaultBits(std::uint32_t input, std::uint32_t component, bool high) const { + if (!high) { + return InputDefaultBits(input, component); + } + const auto value = input < 32u ? (interpolatorSettings[input] >> 21u) & 0x3u : 0u; + const bool one = component == 3u ? (value & 0x1u) != 0u : (value & 0x2u) != 0u; + return one ? 0x3f800000u : 0u; + } + [[nodiscard]] bool InputIsLinear(std::uint32_t input, std::uint32_t linearInputs, std::uint32_t perspectiveInputs) const { const auto bit = input < 32u ? 1u << input : 0u; if ((linearInputs & perspectiveInputs & bit) != 0u) { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index c5d5a3aa5..93f66f1c3 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -106,6 +106,20 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } break; } + case IrOpcode::GetInterpolationParameterF16: { + const IrValue* input = inst->Argument(0)->Resolve(); + const IrValue* component = inst->Argument(1)->Resolve(); + const IrValue* mode = inst->Argument(2)->Resolve(); + const IrValue* high = inst->Argument(3)->Resolve(); + if (program.Resources().stage != IrShaderStage::Pixel || !input->HasImmediate() || input->Type() != IrType::U32 || !component->HasImmediate() || component->Type() != IrType::U32 || + !mode->HasImmediate() || mode->Type() != IrType::U32 || !high->HasImmediate() || high->Type() != IrType::U32) { + return Fail("16-bit interpolation parameter reference is invalid"); + } + if (input->ImmediateU32() >= inputInfo.pixel->inputNum || component->ImmediateU32() >= 4u || mode->ImmediateU32() >= 3u || high->ImmediateU32() >= 2u) { + return Fail("16-bit interpolation parameter reference is out of range"); + } + break; + } case IrOpcode::GetBuiltin: { const IrValue* kindValue = inst->Argument(0)->Resolve(); const IrValue* componentValue = inst->Argument(1)->Resolve(); @@ -219,6 +233,7 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi std::array read {}; std::array perVertex {}; std::array interpolated {}; + std::array halfRead {}; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() == IrOpcode::GetAttribute) { @@ -233,6 +248,14 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; perVertex[input] = perVertex[input] || mode < 2u || !IsPixelParameterFlat(*pixel, input); + } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { + const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); + const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; + if (!pixel->InputHalfIsDefault(input, high)) { + read[input] = true; + perVertex[input] = true; + halfRead[input] = true; + } } } } @@ -248,7 +271,7 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi slotInterpolation.fill(unassigned); std::array slotPerVertex {}; for (std::uint32_t input = 0; input < pixel->inputNum; input++) { - if (!read[input] || pixel->InputIsDefault(input)) { + if (!read[input] || (pixel->InputIsDefault(input) && !halfRead[input])) { continue; } const auto slot = pixel->InputSlot(input); @@ -262,7 +285,7 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi bool smooth = false; bool noPerspective = false; for (std::uint32_t input = 0; input < pixel->inputNum; input++) { - if (!read[input] || pixel->InputIsDefault(input)) { + if (!read[input] || (pixel->InputIsDefault(input) && !halfRead[input])) { continue; } const bool vertexInput = slotPerVertex[pixel->InputSlot(input)]; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index 3be783c0c..61f55bba0 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -1027,6 +1027,9 @@ enum class RdnaOpcode : std::uint16_t { VInterpP1F32, VInterpP2F32, VInterpMovF32, + VInterpP1llF16, + VInterpP1lvF16, + VInterpP2F16, SWaitcntDepctr, SSendmsg, SGetregB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp index a9b5f9a41..79affa1cd 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaOpcode.cpp @@ -623,6 +623,9 @@ bool IsVectorAluOpcode(RdnaOpcode opcode) { case RdnaOpcode::VInterpP1F32: case RdnaOpcode::VInterpP2F32: case RdnaOpcode::VInterpMovF32: + case RdnaOpcode::VInterpP1llF16: + case RdnaOpcode::VInterpP1lvF16: + case RdnaOpcode::VInterpP2F16: return true; default: return false; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 880f08383..326343f03 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -493,6 +493,7 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x357u, RdnaOpcode::VMed3F16}, {0x358u, RdnaOpcode::VMed3I16}, {0x359u, RdnaOpcode::VMed3U16}, + {0x35au, RdnaOpcode::VInterpP2F16}, {0x35fu, RdnaOpcode::VDivFixupF16}, {0x15au, RdnaOpcode::VSadU8}, {0x15bu, RdnaOpcode::VSadHiU8}, @@ -534,6 +535,8 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x311u, RdnaOpcode::VPackB32F16}, {0x314u, RdnaOpcode::VLshlrevB16}, {0x319u, RdnaOpcode::VSubrevI32}, + {0x342u, RdnaOpcode::VInterpP1llF16}, + {0x343u, RdnaOpcode::VInterpP1lvF16}, {0x344u, RdnaOpcode::VPermB32}, {0x14du, RdnaOpcode::VLerpU8}, {0x345u, RdnaOpcode::VXadU32}, @@ -2073,6 +2076,43 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, } } +bool isVop3F16InterpolationOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VInterpP1llF16 || opcode == RdnaOpcode::VInterpP1lvF16 || opcode == RdnaOpcode::VInterpP2F16; +} + +void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { + const bool hasSource2 = instruction.op != RdnaOpcode::VInterpP1llF16; + const bool f16Result = instruction.op == RdnaOpcode::VInterpP2F16; + if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || opSel != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { + throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); + } + if (src1 < 256u || (hasSource2 && src2 < 256u)) { + throw std::invalid_argument("VOP3 interpolation source is not a vector register"); + } + const bool high = ((src0 >> 8u) & 0x1u) != 0u; + instruction.destination = DecodeRdnaVectorGpr(vdst); + instruction.destination.clamp = clamp != 0u; + instruction.destination.omod = omod; + instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); + instruction.source0.absolute = (abs & 0x2u) != 0u; + instruction.source0.negate = (neg & 0x2u) != 0u; + instruction.source1.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source1.value = src0 & 0x3fu; + instruction.source1.signedVal = static_cast(instruction.source1.value); + instruction.source1.opSel = high; + instruction.source2.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source2.value = (src0 >> 6u) & 0x3u; + instruction.source2.signedVal = static_cast(instruction.source2.value); + instruction.sourceCount = 3; + if (hasSource2) { + instruction.source3 = DecodeRdnaVectorGpr(src2 - 256u); + instruction.source3.absolute = (abs & 0x4u) != 0u; + instruction.source3.negate = (neg & 0x4u) != 0u; + instruction.source3.opSel = high && !f16Result; + instruction.sourceCount = 4; + } +} + void applyNativeVop3SourceModifiers(RdnaInstruction& instruction, std::uint32_t abs, std::uint32_t neg) { RdnaOperand* sources[] = {&instruction.source0, &instruction.source1, &instruction.source2}; for (std::uint32_t i = 0; i < instruction.sourceCount && i < 3u; ++i) { @@ -2233,6 +2273,11 @@ RdnaInstruction DecodeRdnaVop3(std::uint32_t programCounter, std::span= PixelParameterLimit || input >= info.inputNum) { FailEmit("pixel interpolator index is out of range"); } - if (info.InputIsDefault(input)) { + if (info.InputIsDefault(input) && (!info.InputIsFp16(input) || info.InputHalfIsDefault(input, true))) { FailEmit("pixel input " + std::to_string(input) + " reads a default value, not a parameter"); } return info.InputSlot(input); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 3e4478aa9..4fb0e39aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -200,6 +200,38 @@ std::uint32_t EmitInterpolationParameterValue(SpirvEmitterState& state, std::uin return bits; } +std::uint32_t EmitInterpolationParameterF16Value(SpirvEmitterState& state, std::uint32_t attr, std::uint32_t chan, std::uint32_t mode, bool high) { + const auto& pixel = *state.inputInfo.pixel; + if (pixel.InputHalfIsDefault(attr, high)) { + return ConstantF32(state, mode == 2u ? pixel.InputHalfDefaultBits(attr, chan & 3u, high) : 0u); + } + const auto* input = SpirvInputBindingForParameter(state, attr); + if (input == nullptr || !input->perVertex) { + throw std::runtime_error("16-bit interpolation parameter is not read per vertex"); + } + const auto loadVertex = [&](std::uint32_t vertex) { + const auto pointer = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, input->variableId, ConstantU32(state, vertex), ConstantU32(state, chan & 3u)); + state.module.AddFunction(spv::OpLoad, TypeF32(state), value, pointer); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + if (high) { + const auto shifted = state.module.AllocateId(); + state.module.AddFunction(spv::OpShiftRightLogical, TypeU32(state), shifted, bits, ConstantU32(state, 16u)); + bits = shifted; + } + return EmitF16BitsToF32(state, bits); + }; + const auto value = loadVertex((mode + 1u) % 3u); + if (mode == 2u) { + return value; + } + const auto delta = state.module.AllocateId(); + state.module.AddFunction(spv::OpFSub, TypeF32(state), delta, value, loadVertex(0u)); + return EmitF16BitsToF32(state, EmitConvertF16F32(state, delta)); +} + std::uint32_t MrtOutputMode(const SpirvEmitterState& state, const ExportInfo& exp) { if (state.program.Resources().stage != IrShaderStage::Pixel || exp.kind != ExportTargetKind::Mrt) { return 0u; @@ -1085,6 +1117,10 @@ std::uint32_t EmitGetInterpolationParameter(SpirvValueEmitContext& ctx, const Ir return EmitInterpolationParameterValue(ctx.state, inst.Argument(0)->ImmediateU32(), inst.Argument(1)->ImmediateU32(), inst.Argument(2)->ImmediateU32()); } +std::uint32_t EmitGetInterpolationParameterF16(SpirvValueEmitContext& ctx, const IrValue& inst) { + return EmitInterpolationParameterF16Value(ctx.state, inst.Argument(0)->ImmediateU32(), inst.Argument(1)->ImmediateU32(), inst.Argument(2)->ImmediateU32(), inst.Argument(3)->ImmediateU32() != 0u); +} + void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto& exp = ctx.Export(inst); diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 5e03d7c6e..8afff17d9 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -298,6 +298,9 @@ private: void vInterpP1F32(const RdnaInstruction& inst); void vInterpP2F32(const RdnaInstruction& inst); void vInterpMovF32(const RdnaInstruction& inst); + IrF32 interpolationParameterF16(const RdnaInstruction& inst, std::uint32_t mode); + void vInterpP1F16(const RdnaInstruction& inst); + void vInterpP2F16(const RdnaInstruction& inst); void eXP(const RdnaInstruction& inst); bool emitScalar(const RdnaInstruction& inst); bool emitVector(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index 32a84d1b4..52e295e3f 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -93,6 +93,31 @@ void TranslationContext::vInterpMovF32(const RdnaInstruction& inst) { writeOperand(inst.destination, &value); } +IrF32 TranslationContext::interpolationParameterF16(const RdnaInstruction& inst, std::uint32_t mode) { + if (!fragmentShaderBarycentricEnabled) { + throw std::runtime_error("16-bit interpolation of pixel input " + std::to_string(inst.source1.value) + " requires fragmentShaderBarycentric"); + } + if (pixelInput == nullptr || !pixelInput->InputIsFp16(inst.source1.value)) { + throw std::runtime_error("pixel input " + std::to_string(inst.source1.value) + " is read with 16-bit interpolation without FP16_INTERP_MODE"); + } + return IrF32(ir.Emit(IrOpcode::GetInterpolationParameterF16, IrType::F32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(mode), &ir.Constant(inst.source1.opSel ? 1u : 0u)})); +} + +void TranslationContext::vInterpP1F16(const RdnaInstruction& inst) { + const IrF32 delta = interpolationParameterF16(inst, 0u); + const IrF32 origin = inst.op == RdnaOpcode::VInterpP1lvF16 ? readF16AsF32(inst.source3) : interpolationParameterF16(inst, 2u); + auto& product = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&delta.Value(), readOperand(inst.source0, IrType::F32)}); + auto& result = ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&product, &origin.Value()}); + writeOperand(inst.destination, &result); +} + +void TranslationContext::vInterpP2F16(const RdnaInstruction& inst) { + const IrF32 delta = interpolationParameterF16(inst, 1u); + auto& product = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&delta.Value(), readOperand(inst.source0, IrType::F32)}); + auto& result = ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&product, readOperand(inst.source3, IrType::F32)}); + writeF16(inst.destination, IrF32(result)); +} + void TranslationContext::eXP(const RdnaInstruction& inst) { std::uint32_t index = 0u; if (exportTargetKindFromTarget(inst.exportTarget, index) == ExportTargetKind::Unknown) { @@ -109,7 +134,8 @@ void TranslationContext::eXP(const RdnaInstruction& inst) { } bool TranslationContext::emitInterpolation(const RdnaInstruction& inst) { - if ((inst.op == RdnaOpcode::VInterpP1F32 || inst.op == RdnaOpcode::VInterpP2F32) && pixelInput != nullptr && pixelInput->InputIsCustom(inst.source1.value)) { + const bool interpolates = inst.op == RdnaOpcode::VInterpP1F32 || inst.op == RdnaOpcode::VInterpP2F32 || inst.op == RdnaOpcode::VInterpP1llF16 || inst.op == RdnaOpcode::VInterpP1lvF16 || inst.op == RdnaOpcode::VInterpP2F16; + if (interpolates && pixelInput != nullptr && pixelInput->InputIsCustom(inst.source1.value)) { throw std::runtime_error("pixel input " + std::to_string(inst.source1.value) + " passes its vertices through unchanged but is read with v_interp_p1/p2"); } switch (inst.op) { @@ -122,6 +148,13 @@ bool TranslationContext::emitInterpolation(const RdnaInstruction& inst) { case RdnaOpcode::VInterpMovF32: vInterpMovF32(inst); return true; + case RdnaOpcode::VInterpP1llF16: + case RdnaOpcode::VInterpP1lvF16: + vInterpP1F16(inst); + return true; + case RdnaOpcode::VInterpP2F16: + vInterpP2F16(inst); + return true; default: return false; } diff --git a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp index e2d3e4322..c77fff4ad 100644 --- a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp +++ b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp @@ -38,7 +38,7 @@ void TranslationContext::TranslateInstruction(const RdnaInstruction& decoded) { case RdnaInstructionFamily::VOP3: case RdnaInstructionFamily::VOP3P: case RdnaInstructionFamily::VOPC: - translated = emitVector(instruction); + translated = emitVector(instruction) || emitInterpolation(instruction); break; case RdnaInstructionFamily::SMEM: case RdnaInstructionFamily::MUBUF: diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c8c7b6efc..e1a575413 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -277,6 +277,7 @@ Throughout the project, every function at every stage either **does exactly what - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). +- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, and each step is a separate multiply and add, as in v_interp_p1_f32 and v_interp_p2_f32; neither is confirmed on hardware. Lavapipe has no barycentrics, so the execution test is skipped in CI. - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. - [SINT storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. - [`StoreTexel`](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944.