[Fix] (Shader): make demoted double varyings flat on every stage so both sides of the interface agree

This commit is contained in:
2026-10-12 06:46:04 -04:00
parent bc3c4fbb6c
commit 106190e2ff
2 changed files with 201 additions and 0 deletions
@@ -463,6 +463,99 @@ void main() {
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput({}));
}
namespace {
constexpr Uint32 kOpDecorate = 71;
constexpr Uint32 kDecorationFlat = 14;
Uint32 IdNamed(const Vector<Uint32>& spirv, const String& name) {
Uint32 id = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpName || wordCount < 3 || id != 0) return;
const char* text = reinterpret_cast<const char*>(&words[2]);
if (std::strncmp(text, name.c_str(), (wordCount - 2) * sizeof(Uint32)) == 0) id = words[1];
});
return id;
}
Bool MemberIsFlat(const Vector<Uint32>& spirv, Uint32 structId, Uint32 member) {
Bool flat = false;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpMemberDecorate && wordCount >= 4 && words[1] == structId && words[2] == member &&
words[3] == kDecorationFlat) {
flat = true;
}
});
return flat;
}
Uint32 CountFlatDecorationsOf(const Vector<Uint32>& spirv, Uint32 id) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpDecorate && wordCount >= 3 && words[1] == id && words[2] == kDecorationFlat) ++count;
});
return count;
}
} // namespace
// A double varying is flat by rule on the fragment side only; once both sides narrow to float a
// smooth vertex output would feed a flat fragment input, which an interface-matching linker
// rejects. The demotion makes every former-double interface member Flat on BOTH stages - and
// leaves vertex inputs alone, where the decoration is invalid (the TearDown's spirv-val holds it).
TEST_F(DemoteFloat64Test, AFormerDoubleVaryingIsFlatOnBothSidesOfTheInterface) {
const String vertex = R"(#version 430 core
layout(location = 0) in vec4 inPos;
layout(location = 3) in dvec3 inData0;
out StageData {
vec2 color;
dvec3 data0;
} vs_out;
layout(location = 5) out double vLoose;
void main() {
vs_out.color = inPos.xy;
vs_out.data0 = inData0;
vLoose = inData0.x;
gl_Position = inPos;
}
)";
const String fragment = R"(#version 430 core
in StageData {
vec2 color;
flat dvec3 data0;
} fs_in;
layout(location = 5) flat in double vLoose;
layout(location = 0) out vec4 fragColor;
void main() {
fragColor = vec4(fs_in.color, float(fs_in.data0.x + vLoose), 1.0);
}
)";
const Vector<Uint32> wideVertex = CompileToSpirv(GL_VERTEX_SHADER, vertex);
const Vector<Uint32> wideFragment = CompileToSpirv(GL_FRAGMENT_SHADER, fragment);
ASSERT_FALSE(wideVertex.empty());
ASSERT_FALSE(wideFragment.empty());
ASSERT_FALSE(MemberIsFlat(wideVertex, IdNamed(wideVertex, "StageData"), 1)) << Disassemble(wideVertex);
Vector<Uint32> narrowVertex;
Vector<Uint32> narrowFragment;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wideVertex, narrowVertex, true));
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wideFragment, narrowFragment, true));
const Uint32 vertexBlock = IdNamed(narrowVertex, "StageData");
const Uint32 fragmentBlock = IdNamed(narrowFragment, "StageData");
ASSERT_NE(vertexBlock, 0u);
ASSERT_NE(fragmentBlock, 0u);
EXPECT_TRUE(MemberIsFlat(narrowVertex, vertexBlock, 1)) << Disassemble(narrowVertex);
EXPECT_TRUE(MemberIsFlat(narrowFragment, fragmentBlock, 1)) << Disassemble(narrowFragment);
// The member that was always a float keeps its interpolation.
EXPECT_FALSE(MemberIsFlat(narrowVertex, vertexBlock, 0));
EXPECT_FALSE(MemberIsFlat(narrowFragment, fragmentBlock, 0));
// A loose varying is decorated on the variable, once - the fragment side already said flat.
EXPECT_EQ(CountFlatDecorationsOf(narrowVertex, IdNamed(narrowVertex, "vLoose")), 1u) << Disassemble(narrowVertex);
EXPECT_EQ(CountFlatDecorationsOf(narrowFragment, IdNamed(narrowFragment, "vLoose")), 1u)
<< Disassemble(narrowFragment);
// Not an interpolant: a vertex input never takes the decoration.
EXPECT_EQ(CountFlatDecorationsOf(narrowVertex, IdNamed(narrowVertex, "inData0")), 0u);
}
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
// one of these before demotion, so the program simply could not be transpiled at all.
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
@@ -19,6 +19,7 @@
#include <algorithm>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
@@ -401,6 +402,72 @@ namespace MobileGL {
return Status::SuccessWithoutChange;
}
// --- 2b. the stage interface that has to become flat --------------------------
// A double varying is never interpolated: GLSL requires `flat` on a fragment input
// of any double type, and nothing on the producing side, so a vertex `out dvec3`
// meets a fragment `flat in dvec3`. Once both narrow to float that is a smooth
// output feeding a flat input - an interpolation mismatch a linker that compares
// the two sides rejects. Every Input / Output that held a 64-bit float is
// therefore made Flat, on every stage, which is the one rule both sides of any
// interface agree on (the same reason integer varyings are forced flat
// everywhere). Collected now, while the wide types are still telling; the
// decorations are written once the rewrite is done. Vertex inputs and fragment
// outputs are not interpolants and must not carry the decoration.
Bool isVertexStage = false;
Bool isFragmentStage = false;
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
const auto model = static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0));
if (model == spv::ExecutionModel::Vertex) isVertexStage = true;
if (model == spv::ExecutionModel::Fragment) isFragmentStage = true;
}
// {target, member}; member == kWholeVariable decorates the variable itself.
constexpr Uint32 kWholeVariable = ~0u;
std::vector<std::pair<Uint32, Uint32>> flatTargets;
for (Instruction& variable : irContext->types_values()) {
if (variable.opcode() != spv::Op::OpVariable) continue;
const auto storageClass = static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
if (storageClass == spv::StorageClass::Input) {
if (isVertexStage) continue;
} else if (storageClass == spv::StorageClass::Output) {
if (isFragmentStage) continue;
} else {
continue;
}
const Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue;
if (wideTypeIds.count(pointerType->GetSingleWordInOperand(1)) == 0) continue;
// A block's interpolation lives on its members, and an arrayed one (`in Blk
// { ... } blk[]` on a geometry or tessellation stage) on the struct underneath.
// A plain struct varying is decorated as a whole: its type may be shared with
// a non-interface use that member decorations would leak into.
const Instruction* pointee = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
pointee = defUseMgr->GetDef(pointee->GetSingleWordInOperand(0));
}
Bool isBlock = false;
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeStruct) {
for (const Instruction& annotation : irContext->annotations()) {
if (annotation.opcode() == spv::Op::OpDecorate &&
annotation.GetSingleWordInOperand(0) == pointee->result_id() &&
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
spv::Decoration::Block) {
isBlock = true;
break;
}
}
}
if (isBlock) {
for (Uint32 member = 0; member < pointee->NumInOperands(); ++member) {
if (wideTypeIds.count(pointee->GetSingleWordInOperand(member)) != 0) {
flatTargets.emplace_back(pointee->result_id(), member);
}
}
} else {
flatTargets.emplace_back(variable.result_id(), kWholeVariable);
}
}
// --- 3. re-encode the literals ----------------------------------------------
// A 64-bit float constant carries two literal words and a 32-bit one carries a
// single word, so the value has to be narrowed before the type changes underneath
@@ -577,6 +644,47 @@ namespace MobileGL {
}
}
// --- 9. the flat interface collected in 2b ------------------------------------
// A target already Flat (a fragment input the source qualified) keeps its one
// decoration; a second one would be a duplicate.
const auto alreadyFlat = [&](Uint32 target, Uint32 member) {
for (const Instruction& annotation : irContext->annotations()) {
if (member == kWholeVariable) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (annotation.GetSingleWordInOperand(0) != target) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
spv::Decoration::Flat) {
return true;
}
} else {
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
if (annotation.GetSingleWordInOperand(0) != target) continue;
if (annotation.GetSingleWordInOperand(1) != member) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
spv::Decoration::Flat) {
return true;
}
}
}
return false;
};
for (const auto& [target, member] : flatTargets) {
if (alreadyFlat(target, member)) continue;
const Uint32 flat = static_cast<Uint32>(spv::Decoration::Flat);
if (member == kWholeVariable) {
irContext->AddAnnotationInst(std::make_unique<Instruction>(
irContext, spv::Op::OpDecorate, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {target}},
{SPV_OPERAND_TYPE_DECORATION, {flat}}}));
} else {
irContext->AddAnnotationInst(std::make_unique<Instruction>(
irContext, spv::Op::OpMemberDecorate, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {target}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {member}},
{SPV_OPERAND_TYPE_DECORATION, {flat}}}));
}
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}