mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
Without VK_KHR_fragment_shader_barycentric the recompiler dropped v_interp_p1_f32 and made v_interp_p2_f32 read the attribute Vulkan interpolated, but never wrote the I/J VGPRs: a pixel shader that read them for anything else got 0, and v_interp_mov_f32 of P10/P20 (or P0 of an interpolated input) needed PerVertexKHR and failed pipeline validation. The fallback translation now loads the I/J VGPRs and marks v_interp_p1 and v_interp_p2 with witnesses. After the front-end simplification (SSA, folding, DCE, read-lane and masked-select elimination), HostInterpolationChecker requires every p2 to read its J, and its p1's I, from a barycentric of the parameter's kind, every p1 result to reach only the matching p2 (through a write masked by the p2's EXEC, whose inactive lanes keep the old register), no parameter read as raw vertices and no slot shared by different interpolations. It then lowers the witnesses to the host-interpolated attribute as before, reruns DCE and masked-select elimination, and accepts the program only when no barycentric read remains, so reusing the I/J VGPRs after interpolating keeps host interpolation and the previous SPIR-V. Any other program is translated with the barycentric path and marked as emulated: its barycentrics become vec3 inputs (NoPerspective for the linear weights) and its per-vertex parameters flat vec4[3] inputs, placed in the free locations by LayoutBarycentricEmulation. The driver then inserts a generated geometry stage between the vertex and fragment shaders that copies the position and parameters, writes the one-hot weights of each vertex and the three vertices' parameter values, so the rasterizer interpolates the same weights the extension provides. The stage needs geometryShader and a vertex shader before it; tessellation, mesh and rect-list pipelines, other primitive types and interfaces beyond 32 locations or the device's geometry limits throw. CompiledShaderArtifact carries the emulation (shader disk cache format 21). Hardware snaps window corners to 1/256 pixel before interpolating (measured by yahya308 on an RTX 4070 Laptop and a Radeon 780M in #1547); lavapipe snaps only for coverage (lp_rast.h FIXED_ORDER 8), so the test accepts either reference per pixel at the original 1e-4 margin. Sources: - v_interp_p1_f32/v_interp_p2_f32/v_interp_mov_f32 semantics (P0, P10, P20, I/J): AMD RDNA2 ISA, "Parameter Interpolation". - BaryCoordKHR/BaryCoordNoPerspKHR weights and PerVertexKHR vertex order: SPV_KHR_fragment_shader_barycentric, VK_KHR_fragment_shader_barycentric. - Geometry shader triangle input order and provoking vertex: Vulkan specification, "Primitive Topologies" and "Geometry Shading". - Interpolation decorations decided by the fragment input: Vulkan specification, "Interpolation Decorations". - lavapipe interpolation: Mesa src/gallium/drivers/llvmpipe/lp_rast.h (FIXED_ORDER 8), lp_setup_tri.c (subpixel_snap for coverage only), lp_state_setup.c (init_args, plane setup from float positions).
143 lines
6.4 KiB
C++
143 lines
6.4 KiB
C++
#include "Optimization/HostInterpolationChecker.hpp"
|
|
#include "IntermediateRepresentation/IrBlock.hpp"
|
|
#include "IntermediateRepresentation/IrMetadata/StageIO.hpp"
|
|
#include "Optimization/DeadCodeEliminator.hpp"
|
|
#include "Optimization/MaskedSelectEliminator.hpp"
|
|
#include <array>
|
|
#include <vector>
|
|
|
|
namespace ShaderRecompiler {
|
|
namespace {
|
|
|
|
bool isCopy(const IrValue& value) {
|
|
return value.IsIdentity() || value.Opcode() == IrOpcode::BitCastF32U32 || value.Opcode() == IrOpcode::BitCastU32F32;
|
|
}
|
|
|
|
const IrValue* lookThrough(const IrValue* value) {
|
|
value = value->Resolve();
|
|
while (value->Opcode() == IrOpcode::BitCastF32U32 || value->Opcode() == IrOpcode::BitCastU32F32) value = value->Argument(0)->Resolve();
|
|
return value;
|
|
}
|
|
|
|
const IrValue* partialOf(const IrValue& interpolation) {
|
|
const auto* exec = interpolation.Argument(4)->Resolve();
|
|
const auto* value = lookThrough(interpolation.Argument(3));
|
|
while (value->Opcode() == IrOpcode::SelectU32) {
|
|
if (value->Argument(0)->Resolve() != exec) return value;
|
|
value = lookThrough(value->Argument(1));
|
|
}
|
|
return value;
|
|
}
|
|
|
|
bool isBarycentric(const IrValue& value) {
|
|
if (value.Opcode() != IrOpcode::GetBuiltin) return false;
|
|
const auto kind = static_cast<StageInputKind>(value.Argument(0)->Resolve()->ImmediateU32());
|
|
return kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective;
|
|
}
|
|
|
|
bool isBarycentric(const IrValue& value, StageInputKind kind, std::uint32_t component) {
|
|
return isBarycentric(value) && static_cast<StageInputKind>(value.Argument(0)->Resolve()->ImmediateU32()) == kind && value.Argument(1)->Resolve()->ImmediateU32() == component;
|
|
}
|
|
|
|
template<typename TAccept>
|
|
bool usesAll(const IrValue& value, const TAccept& accept) {
|
|
for (const auto& use : value.OperandUses()) {
|
|
if (isCopy(*use.user)) {
|
|
if (!usesAll(*use.user, accept)) return false;
|
|
} else if (!accept(*use.user, use.operand)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool sameCondition(const IrValue& left, const IrValue& right) {
|
|
return left.Argument(0)->Resolve() == right.Argument(0)->Resolve();
|
|
}
|
|
|
|
bool partialSelect(const IrValue& select) {
|
|
return usesAll(select, [&](const IrValue& user, std::size_t operand) {
|
|
return (user.Opcode() == IrOpcode::InterpolateHostP2 && operand == 3u) || (user.Opcode() == IrOpcode::SelectU32 && operand == 2u && sameCondition(user, select));
|
|
});
|
|
}
|
|
|
|
bool sameParameter(const IrValue& left, const IrValue& right) {
|
|
return left.Argument(0)->Resolve()->ImmediateU32() == right.Argument(0)->Resolve()->ImmediateU32() && left.Argument(1)->Resolve()->ImmediateU32() == right.Argument(1)->Resolve()->ImmediateU32();
|
|
}
|
|
|
|
bool exact(const IrValue& inst, const IrProgram& program, const ShaderPixelInputInfo& pixel, std::vector<IrValue*>& selects) {
|
|
if (inst.Opcode() == IrOpcode::InterpolateHostP1) {
|
|
return usesAll(inst, [&](const IrValue& user, std::size_t operand) {
|
|
if (user.Opcode() == IrOpcode::InterpolateHostP2 && operand == 3u) return true;
|
|
if (user.Opcode() != IrOpcode::SelectU32 || operand != 1u || !partialSelect(user)) return false;
|
|
selects.push_back(const_cast<IrValue*>(&user));
|
|
return true;
|
|
});
|
|
}
|
|
if (inst.Opcode() == IrOpcode::GetInterpolationParameter) {
|
|
const auto input = inst.Argument(0)->Resolve()->ImmediateU32();
|
|
return pixel.InputIsDefault(input) || (inst.Argument(2)->Resolve()->ImmediateU32() == 2u && pixel.InputIsFlat(input));
|
|
}
|
|
if (inst.Opcode() != IrOpcode::InterpolateHostP2) return true;
|
|
const auto* partial = partialOf(inst);
|
|
if (partial->Opcode() != IrOpcode::InterpolateHostP1 || !sameParameter(inst, *partial)) return false;
|
|
const auto& metadata = program.Metadata();
|
|
const auto kind = pixel.InputIsLinear(inst.Argument(0)->Resolve()->ImmediateU32(), metadata.pixelLinearInputs, metadata.pixelPerspectiveInputs) ? StageInputKind::BaryCoordNoPerspective : StageInputKind::BaryCoordSmooth;
|
|
return isBarycentric(*lookThrough(partial->Argument(2)), kind, 0u) && isBarycentric(*lookThrough(inst.Argument(2)), kind, 1u);
|
|
}
|
|
|
|
bool sharesSlots(const IrProgram& program, const ShaderPixelInputInfo& pixel) {
|
|
constexpr std::uint32_t unassigned = 3u;
|
|
std::array<std::uint32_t, 32> interpolation {};
|
|
interpolation.fill(unassigned);
|
|
const auto& metadata = program.Metadata();
|
|
for (const auto& block : program.Blocks()) {
|
|
for (const IrValue* inst : block->Instructions()) {
|
|
if (inst->Opcode() != IrOpcode::GetAttribute && inst->Opcode() != IrOpcode::GetInterpolationParameter) continue;
|
|
const auto input = inst->Argument(0)->Resolve()->ImmediateU32();
|
|
if (input >= pixel.inputNum || pixel.InputIsDefault(input)) continue;
|
|
const auto mode = pixel.InputIsFlat(input) ? 2u : pixel.InputIsLinear(input, metadata.pixelLinearInputs, metadata.pixelPerspectiveInputs) ? 1u : 0u;
|
|
auto& slot = interpolation[pixel.InputSlot(input)];
|
|
if (slot != unassigned && slot != mode) return true;
|
|
slot = mode;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
bool HostInterpolationChecker::Lower(IrProgram& program, const ShaderPixelInputInfo& pixel) const {
|
|
std::vector<IrValue*> selects;
|
|
for (const auto& block : program.Blocks()) {
|
|
for (const IrValue* inst : block->Instructions()) {
|
|
if (!exact(*inst, program, pixel, selects)) return false;
|
|
}
|
|
}
|
|
if (sharesSlots(program, pixel)) return false;
|
|
for (auto* select : selects) select->ReplaceAllUsesWith(select->Argument(2));
|
|
for (const auto& block : program.Blocks()) {
|
|
auto& instructions = block->Instructions();
|
|
for (auto it = instructions.begin(); it != instructions.end();) {
|
|
IrValue* inst = *it;
|
|
if (inst->Opcode() != IrOpcode::InterpolateHostP2) {
|
|
++it;
|
|
continue;
|
|
}
|
|
inst->Invalidate();
|
|
inst->SetParent(nullptr);
|
|
it = instructions.erase(it);
|
|
}
|
|
}
|
|
DeadCodeEliminator{}.Eliminate(program);
|
|
if (MaskedSelectEliminator{}.Eliminate(program).removedSelects != 0u) DeadCodeEliminator{}.Eliminate(program);
|
|
for (const auto& block : program.Blocks()) {
|
|
for (const IrValue* inst : block->Instructions()) {
|
|
if (isBarycentric(*inst) || inst->Opcode() == IrOpcode::InterpolateHostP1) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|