mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Shader): make demoted double varyings flat on every stage so both sides of the interface agree
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user