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 <noreply@anthropic.com>
This commit is contained in:
Daan Dehing 2026-10-05 16:24:25 +00:00 committed by Adrià Franch
parent aa258760dc
commit daa6bee7cf
18 changed files with 486 additions and 5 deletions

View File

@ -1172,6 +1172,19 @@ void verifyPixelParameterSlots() {
expectFailure([] { static_cast<void>(recompileSlots({0x423u, 0x3u}, shared)); }, "passes its vertices through unchanged", "an interpolated pass-through input was accepted");
}
void verifyF16PixelParameterSlots() {
static constexpr std::array<std::uint32_t, 7> high{0xd7420002u, 0x00020100u, 0xd75a0003u, 0x040a0300u, 0xf800180fu, 0x03030303u, 0xbf810000u};
static constexpr std::array<std::uint32_t, 7> 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();

View File

@ -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 <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
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<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<std::uint32_t, Width * Height * ResultDwords> Results{};
alignas(256) constexpr std::array<std::uint32_t, 10> VertexCode{
0xe0382000u, 0x80000005u, 0xe0382010u, 0x80000605u, 0xbf8c3f70u, 0xf800020fu, 0x09080706u, 0xf80008cfu, 0x03020100u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 47> 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<std::uint32_t, 3> LowX{0x3400u, 0x3f00u, 0xb800u};
constexpr std::array<std::uint32_t, 3> HighX{0x4200u, 0xc100u, 0x4700u};
constexpr std::array<std::uint32_t, 3> LowY{0x3800u, 0x3a00u, 0x3000u};
constexpr std::array<std::uint32_t, 3> HighY{0x6c00u, 0x3c00u, 0xc800u};
struct Vertex {
std::array<float, 4> position;
std::array<std::uint32_t, 4> parameter;
};
float HalfToFloat(std::uint32_t half) {
const auto sign = (half & 0x8000u) != 0u ? -1.0f : 1.0f;
const auto exponent = static_cast<int>((half >> 10u) & 0x1fu);
const auto mantissa = static_cast<float>(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<std::uint32_t>(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::uint32_t>(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<Vertex> Triangles() {
std::vector<Vertex> 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<float>(cell % columns);
const float y = -1.0f + height * static_cast<float>(cell / columns);
const float skew = 0.07f * static_cast<float>(cell % 5u);
const std::array<std::array<float, 2>, 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<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((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<const std::uint32_t> code, std::uint32_t waveSize, std::uint32_t input0, const ShaderRecompiler::SpirvTarget& target, std::uint32_t pushOffset) {
std::vector<std::uint32_t> 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<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(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<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(triangles.data(), sizeof(Vertex), static_cast<std::uint32_t>(triangles.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(vertexResult.pushConstants.size());
const auto pixelResult = RecompilePixel(PixelCode, waveSize, Fp16Input, target, vertexPush);
const std::array<AgcDriver::Graphics::CompiledShader, 2> 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<std::uintptr_t>(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<float>(Height), static_cast<float>(Width), -static_cast<float>(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<std::uint32_t>(triangles.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
float Delta(const std::array<std::uint32_t, 3>& 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<std::uint8_t>(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<float>(result[0]);
const float j = std::bit_cast<float>(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<std::uint32_t, ResultDwords> expected{
result[0],
result[1],
std::bit_cast<std::uint32_t>(lowX),
0xabcd0000u | FloatToHalf(SecondStep(Delta(LowX, 2), j, lowX)),
0x12340000u | FloatToHalf(SecondStep(Delta(HighX, 2), j, highX)),
std::bit_cast<std::uint32_t>(highY),
FloatToHalf(SecondStep(Delta(HighY, 2), j, highY)),
FloatToHalf(Saturate(SecondStep(Delta(LowX, 2), j, lowX))),
std::bit_cast<std::uint32_t>(FirstStep(0.0f, i, 1.0f)),
std::bit_cast<std::uint32_t>(FirstStep(0.0f, i, 0.0f)),
std::bit_cast<std::uint32_t>(FirstStep(Delta(LowX, 1), -i, HalfToFloat(LowX[0]))),
std::bit_cast<std::uint32_t>(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<double>(i), static_cast<double>(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<const std::uint32_t> code, std::uint32_t input0, bool barycentric, const char* expected, const char* message) {
try {
static_cast<void>(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<std::uint32_t, 3> 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;
}
}

View File

@ -457,6 +457,7 @@ enum class IrOpcode : std::uint16_t {
ImageAtomicCmpSwap64,
GetAttribute,
GetInterpolationParameter,
GetInterpolationParameterF16,
SetAttribute,
ControlNop,
Waitcnt,

View File

@ -498,6 +498,7 @@ constexpr std::array<OpcodeMeta, static_cast<std::size_t>(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),

View File

@ -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) {

View File

@ -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<bool, 32> read {};
std::array<bool, 32> perVertex {};
std::array<bool, 32> interpolated {};
std::array<bool, 32> 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<bool, 32> 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)];

View File

@ -1027,6 +1027,9 @@ enum class RdnaOpcode : std::uint16_t {
VInterpP1F32,
VInterpP2F32,
VInterpMovF32,
VInterpP1llF16,
VInterpP1lvF16,
VInterpP2F16,
SWaitcntDepctr,
SSendmsg,
SGetregB32,

View File

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

View File

@ -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<std::int32_t>(instruction.source1.value);
instruction.source1.opSel = high;
instruction.source2.kind = RdnaOperandKind::IntegerInlineConstant;
instruction.source2.value = (src0 >> 6u) & 0x3u;
instruction.source2.signedVal = static_cast<std::int32_t>(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<const std
instruction.op = lookupVop3Opcode(opcode);
SetRdnaRawWords(instruction, code, wordIndex, 2);
if (isVop3F16InterpolationOpcode(instruction.op)) {
decodeVop3F16Interpolation(instruction, vdst, src0, src1, src2, abs, opSel, clamp, omod, neg);
return instruction;
}
const bool carryInOut = (instruction.op == RdnaOpcode::VAddcU32 && opcode == 0x128u) ||
(instruction.op == RdnaOpcode::VSubCoCiU32 && opcode == 0x129u) ||
(instruction.op == RdnaOpcode::VSubrevCoCiU32 && opcode == 0x12au);

View File

@ -78,6 +78,7 @@ std::uint32_t EmitWriteLane(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetInterpolationParameter(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetInterpolationParameterF16(SpirvValueEmitContext& ctx, const IrValue& inst);
void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst);
std::uint32_t EmitGetShaderBase(SpirvValueEmitContext& ctx);
std::uint32_t EmitShaderClock(SpirvValueEmitContext& ctx, const IrValue& inst);

View File

@ -37,6 +37,7 @@ bool LaneSource(const IrProgram& program, const IrValue& value) {
case IrOpcode::LaneId:
case IrOpcode::GetAttribute:
case IrOpcode::GetInterpolationParameter:
case IrOpcode::GetInterpolationParameterF16:
case IrOpcode::GetTessellationAttribute:
case IrOpcode::DppMoveU32:
case IrOpcode::DppUpdateU32:

View File

@ -672,6 +672,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) {
case IrOpcode::ImageAtomicCmpSwap64: return Invoke(EmitImageAtomicCmpSwap64, ctx, inst);
case IrOpcode::GetAttribute: return Invoke(EmitGetAttribute, ctx, inst);
case IrOpcode::GetInterpolationParameter: return Invoke(EmitGetInterpolationParameter, ctx, inst);
case IrOpcode::GetInterpolationParameterF16: return Invoke(EmitGetInterpolationParameterF16, ctx, inst);
case IrOpcode::SetAttribute: return Invoke(EmitSetAttribute, ctx, inst);
case IrOpcode::ControlNop: return Invoke(EmitControlNop, ctx, inst);
case IrOpcode::Waitcnt: return Invoke(EmitWaitcnt, ctx, inst);

View File

@ -28,7 +28,7 @@ namespace ShaderRecompiler
if (input >= 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);

View File

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

View File

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

View File

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

View File

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

View File

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