diff --git a/MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp b/MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp index 41851041..4b99224b 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp @@ -463,6 +463,99 @@ void main() { EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput({})); } +namespace { + constexpr Uint32 kOpDecorate = 71; + constexpr Uint32 kDecorationFlat = 14; + + Uint32 IdNamed(const Vector& 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(&words[2]); + if (std::strncmp(text, name.c_str(), (wordCount - 2) * sizeof(Uint32)) == 0) id = words[1]; + }); + return id; + } + + Bool MemberIsFlat(const Vector& 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& 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 wideVertex = CompileToSpirv(GL_VERTEX_SHADER, vertex); + const Vector 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 narrowVertex; + Vector 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 {}; diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp index 7191fd69..f9f8fef8 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -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(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> flatTargets; + for (Instruction& variable : irContext->types_values()) { + if (variable.opcode() != spv::Op::OpVariable) continue; + const auto storageClass = static_cast(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(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(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(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(spv::Decoration::Flat); + if (member == kWholeVariable) { + irContext->AddAnnotationInst(std::make_unique( + irContext, spv::Op::OpDecorate, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {target}}, + {SPV_OPERAND_TYPE_DECORATION, {flat}}})); + } else { + irContext->AddAnnotationInst(std::make_unique( + irContext, spv::Op::OpMemberDecorate, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {target}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {member}}, + {SPV_OPERAND_TYPE_DECORATION, {flat}}})); + } + } + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); return Status::SuccessWithChange; }