// // Copyright 2023 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // EmulateFramebufferFetch.h: Replace input, gl_LastFragData and gl_LastFragColorARM with usages of // input attachments. // #include "compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.h" #include "common/bitset_utils.h" #include "compiler/translator/Compiler.h" #include "compiler/translator/ImmutableStringBuilder.h" #include "compiler/translator/SymbolTable.h" #include "compiler/translator/tree_util/BuiltIn.h" #include "compiler/translator/tree_util/IntermNode_util.h" #include "compiler/translator/tree_util/IntermTraverse.h" #include "compiler/translator/tree_util/ReplaceVariable.h" #include "compiler/translator/tree_util/RunAtTheBeginningOfShader.h" #include "compiler/translator/util.h" namespace sh { namespace { using InputAttachmentIndexUsage = angle::BitSet<32>; // A traverser that looks at which inout variables exist, which gl_LastFragData indices have been // used and whether gl_LastFragColorARM is referenced. It builds a set of indices correspondingly; // these are input attachment indices the shader may read from. class InputAttachmentUsageTraverser : public TIntermTraverser { public: InputAttachmentUsageTraverser(uint32_t maxDrawBuffers, TVector *attachmentTypes) : TIntermTraverser(true, false, false), mMaxDrawBuffers(maxDrawBuffers), mAttachmentTypes(attachmentTypes), mUsesLastFragColorARM(false) { mAttachmentTypes->resize(maxDrawBuffers, nullptr); } bool visitDeclaration(Visit visit, TIntermDeclaration *node) override; bool visitBinary(Visit visit, TIntermBinary *node) override; void visitSymbol(TIntermSymbol *node) override; InputAttachmentIndexUsage getIndexUsage() const { return mIndexUsage; } bool usesLastFragColorARM() const { return mUsesLastFragColorARM; } private: void setInputAttachmentIndex(uint32_t index, const TType *type); uint32_t mMaxDrawBuffers; InputAttachmentIndexUsage mIndexUsage; TVector *mAttachmentTypes; bool mUsesLastFragColorARM; }; void InputAttachmentUsageTraverser::setInputAttachmentIndex(uint32_t index, const TType *type) { ASSERT(index < mMaxDrawBuffers); mIndexUsage.set(index); (*mAttachmentTypes)[index] = type; } bool InputAttachmentUsageTraverser::visitDeclaration(Visit visit, TIntermDeclaration *node) { const TIntermSequence &sequence = *node->getSequence(); ASSERT(sequence.size() == 1); TIntermSymbol *symbol = sequence.front()->getAsSymbolNode(); if (symbol == nullptr) { return true; } if (symbol->getQualifier() == EvqFragmentInOut) { ASSERT(symbol->getType().getLayoutQualifier().index <= 0); // The input attachment index is identical to the location qualifier. If there's only one // output, GLSL is allowed to not specify the location qualifier, in which case it would // implicitly be at location 0. const TType &type = symbol->getType(); const unsigned int baseInputAttachmentIndex = std::max(0, type.getLayoutQualifier().location); uint32_t arraySize = type.isArray() ? type.getOutermostArraySize() : 1; for (unsigned int index = 0; index < arraySize; index++) { setInputAttachmentIndex(baseInputAttachmentIndex + index, &type); } } return false; } bool InputAttachmentUsageTraverser::visitBinary(Visit visit, TIntermBinary *node) { TOperator op = node->getOp(); if (op != EOpIndexDirect && op != EOpIndexIndirect) { return true; } TIntermSymbol *left = node->getLeft()->getAsSymbolNode(); if (left == nullptr || left->getQualifier() != EvqLastFragData) { return true; } ASSERT(left->getName() == "gl_LastFragData"); const TType &type = left->getType(); const TConstantUnion *constIndex = node->getRight()->getConstantValue(); // Non-const indices on gl_LastFragData are not allowed. ASSERT(constIndex != nullptr); uint32_t index = 0; switch (constIndex->getType()) { case EbtInt: index = constIndex->getIConst(); break; case EbtUInt: index = constIndex->getUConst(); break; case EbtFloat: index = static_cast(constIndex->getFConst()); break; case EbtBool: index = constIndex->getBConst() ? 1 : 0; break; default: UNREACHABLE(); break; } setInputAttachmentIndex(index, &type); return true; } void InputAttachmentUsageTraverser::visitSymbol(TIntermSymbol *symbol) { if (symbol->getQualifier() != EvqLastFragColor) { return; } ASSERT(symbol->getName() == "gl_LastFragColorARM"); // gl_LastFragColorARM always reads back from location 0. setInputAttachmentIndex(0, &symbol->getType()); mUsesLastFragColorARM = true; } ImmutableString GetInputAttachmentName(size_t index) { std::stringstream nameStream = sh::InitializeStream(); nameStream << "ANGLEInputAttachment" << index; return ImmutableString(nameStream.str()); } TBasicType GetBasicTypeForSubpassInput(TBasicType inputType) { switch (inputType) { case EbtFloat: return EbtSubpassInput; case EbtInt: return EbtISubpassInput; case EbtUInt: return EbtUSubpassInput; default: UNREACHABLE(); return EbtVoid; } } // Declare an input attachment variable at a given index. void DeclareInputAttachmentVariable(TSymbolTable *symbolTable, const TType &outputType, size_t index, TVector *inputAttachmentVarsOut, TIntermSequence *declarationsOut) { const TBasicType subpassInputType = GetBasicTypeForSubpassInput(outputType.getBasicType()); TType *inputAttachmentType = new TType(subpassInputType, outputType.getPrecision(), EvqUniform, 1); TLayoutQualifier inputAttachmentQualifier = inputAttachmentType->getLayoutQualifier(); inputAttachmentQualifier.inputAttachmentIndex = static_cast(index); inputAttachmentType->setLayoutQualifier(inputAttachmentQualifier); (*inputAttachmentVarsOut)[index] = new TVariable( symbolTable, GetInputAttachmentName(index), inputAttachmentType, SymbolType::AngleInternal); TIntermDeclaration *decl = new TIntermDeclaration; decl->appendDeclarator(new TIntermSymbol((*inputAttachmentVarsOut)[index])); declarationsOut->push_back(decl); } // Declare a global variable to hold gl_LastFragData/gl_LastFragColorARM const TVariable *DeclareLastFragDataGlobalVariable(TCompiler *compiler, TIntermBlock *root, const TVector &attachmentTypes, TIntermSequence *declarationsOut) { // Find the first input attachment that is used. If gl_LastFragColorARM was used, this will be // index 0. Otherwise if this is ES100, any index of gl_LastFragData may be used. Either way, // the global variable is declared the same as gl_LastFragData would have been if used. const TType *attachmentType = nullptr; for (const TType *type : attachmentTypes) { if (type != nullptr) { attachmentType = type; break; } } ASSERT(attachmentType != nullptr); TType *globalType = new TType(*attachmentType); globalType->setQualifier(EvqGlobal); // If the type of gl_LastFragColorARM is found, convert it to an array to match gl_LastFragData. // This is necessary if the shader uses both gl_LastFragData and gl_LastFragColorARM // simultaneously. if (!globalType->isArray()) { globalType->makeArray(compiler->getBuiltInResources().MaxDrawBuffers); } // Declare the global const TVariable *global = new TVariable(&compiler->getSymbolTable(), ImmutableString("ANGLELastFragData"), globalType, SymbolType::AngleInternal); TIntermDeclaration *decl = new TIntermDeclaration; decl->appendDeclarator(new TIntermSymbol(global)); declarationsOut->push_back(decl); return global; } // Declare an input attachment for each used index. Additionally, create a global variable for // gl_LastFragData and gl_LastFragColorARM if needed. [[nodiscard]] bool DeclareVariables(TCompiler *compiler, TIntermBlock *root, InputAttachmentIndexUsage indexUsage, bool usesLastFragData, const TVector &attachmentTypes, TVector *inputAttachmentVarsOut, const TVariable **lastFragDataOut) { TSymbolTable *symbolTable = &compiler->getSymbolTable(); TIntermSequence declarations; inputAttachmentVarsOut->resize(indexUsage.last() + 1, nullptr); // For every detected index, declare an input attachment variable. for (size_t index : indexUsage) { ASSERT(attachmentTypes[index] != nullptr); DeclareInputAttachmentVariable(symbolTable, *attachmentTypes[index], index, inputAttachmentVarsOut, &declarations); } // If gl_LastFragData or gl_LastFragColorARM is used, declare a global variable to retain that. // The difference between ES300+ inout variables and gl_LastFrag* is that if the inout variable // is read back after being written to, it should contain the latest value written to it, while // gl_LastFrag* should contain the value before the fragment shader's invocation. // // As such, it is enough to initialize inout variables with the values from input attachments, // but gl_LastFrag* needs to be stored in a global variable to retain its value even after // gl_Frag* has been overwritten. *lastFragDataOut = nullptr; if (usesLastFragData) { *lastFragDataOut = DeclareLastFragDataGlobalVariable(compiler, root, attachmentTypes, &declarations); } // Add the declarations to the beginning of the shader. TIntermSequence &topLevel = *root->getSequence(); declarations.insert(declarations.end(), topLevel.begin(), topLevel.end()); topLevel = std::move(declarations); return compiler->validateAST(root); } TIntermTyped *CreateSubpassLoadFuncCall(TSymbolTable *symbolTable, const TVariable *inputVariable) { TIntermSequence args = {new TIntermSymbol(inputVariable)}; return CreateBuiltInFunctionCallNode("subpassLoad", &args, *symbolTable, kESSLInternalBackendBuiltIns); } void GatherInoutVariables(TIntermBlock *root, TVector *inoutVariablesOut) { TIntermSequence &topLevel = *root->getSequence(); for (TIntermNode *node : topLevel) { TIntermDeclaration *decl = node->getAsDeclarationNode(); if (decl != nullptr) { ASSERT(decl->getSequence()->size() == 1); TIntermSymbol *symbol = decl->getSequence()->front()->getAsSymbolNode(); if (symbol != nullptr && symbol->getQualifier() == EvqFragmentInOut) { ASSERT(symbol->getType().getLayoutQualifier().index <= 0); inoutVariablesOut->push_back(&symbol->variable()); } } } } void InitializeFromInputAttachmentIfPresent(TSymbolTable *symbolTable, TIntermBlock *block, const TVariable *inputVariable, const TVariable *assignVariable, uint32_t assignVariableArrayIndex) { if (inputVariable == nullptr) { // No input variable usage for this index return; } TIntermTyped *var = new TIntermSymbol(assignVariable); if (var->getType().isArray()) { var = new TIntermBinary(EOpIndexDirect, var, CreateIndexNode(assignVariableArrayIndex)); } TIntermTyped *input = CreateSubpassLoadFuncCall(symbolTable, inputVariable); const int vecSize = assignVariable->getType().getNominalSize(); if (vecSize < 4) { TVector swizzle = {0, 1, 2, 3}; swizzle.resize(vecSize); input = new TIntermSwizzle(input, swizzle); } TIntermTyped *assignment = new TIntermBinary(EOpAssign, var, input); block->appendStatement(assignment); } [[nodiscard]] bool InitializeFromInputAttachments( TCompiler *compiler, TIntermBlock *root, const TVector &inputAttachmentVars, const TVector &inoutVariables, const TVariable *lastFragData) { TSymbolTable *symbolTable = &compiler->getSymbolTable(); TIntermBlock *init = new TIntermBlock; // Initialize inout variables for (const TVariable *inoutVar : inoutVariables) { const TType &type = inoutVar->getType(); const unsigned int baseInputAttachmentIndex = std::max(0, type.getLayoutQualifier().location); uint32_t arraySize = type.isArray() ? type.getOutermostArraySize() : 1; for (unsigned int index = 0; index < arraySize; index++) { InitializeFromInputAttachmentIfPresent( symbolTable, init, inputAttachmentVars[baseInputAttachmentIndex + index], inoutVar, index); } } // Initialize lastFragData, if present if (lastFragData != nullptr) { for (uint32_t index = 0; index < inputAttachmentVars.size(); ++index) { InitializeFromInputAttachmentIfPresent(symbolTable, init, inputAttachmentVars[index], lastFragData, index); } } return RunAtTheBeginningOfShader(compiler, root, init); } [[nodiscard]] bool ReplaceVariables(TCompiler *compiler, TIntermBlock *root, const TVector &inputAttachmentVars, const TVariable *lastFragData) { TSymbolTable *symbolTable = &compiler->getSymbolTable(); TVector inoutVariables; GatherInoutVariables(root, &inoutVariables); // Generate code that initializes the global variable and the inout variables with corresponding // input attachments. if (!InitializeFromInputAttachments(compiler, root, inputAttachmentVars, inoutVariables, lastFragData)) { return false; } // Build a map from: // // - inout to out variables // - gl_LastFragData to lastFragData // - gl_LastFragColorARM to lastFragData[0] VariableReplacementMap replacementMap; for (const TVariable *var : inoutVariables) { TType *outType = new TType(var->getType()); outType->setQualifier(EvqFragmentOut); const TVariable *replacement = new TVariable(symbolTable, var->name(), outType, var->symbolType()); replacementMap[var] = new TIntermSymbol(replacement); } if (lastFragData != nullptr) { // Use the user-defined variables if found (and remove their declaration), or the built-in // otherwise. TIntermSequence &topLevel = *root->getSequence(); TIntermSequence newTopLevel; const TVariable *glLastFragData = nullptr; const TVariable *glLastFragColor = nullptr; for (TIntermNode *node : topLevel) { TIntermDeclaration *decl = node->getAsDeclarationNode(); if (decl != nullptr) { ASSERT(decl->getSequence()->size() == 1); TIntermSymbol *symbol = decl->getSequence()->front()->getAsSymbolNode(); if (symbol != nullptr) { if (symbol->getQualifier() == EvqLastFragData) { glLastFragData = &symbol->variable(); continue; } if (symbol->getQualifier() == EvqLastFragColor) { glLastFragColor = &symbol->variable(); continue; } } } newTopLevel.push_back(node); } topLevel = std::move(newTopLevel); if (glLastFragData == nullptr) { glLastFragData = static_cast( symbolTable->findBuiltIn(ImmutableString("gl_LastFragData"), 100)); } if (glLastFragColor == nullptr) { glLastFragColor = static_cast(symbolTable->findBuiltIn( ImmutableString("gl_LastFragColorARM"), compiler->getShaderVersion())); } replacementMap[glLastFragData] = new TIntermSymbol(lastFragData); replacementMap[glLastFragColor] = new TIntermBinary(EOpIndexDirect, new TIntermSymbol(lastFragData), CreateIndexNode(0)); } // Replace the variables accordingly. return ReplaceVariables(compiler, root, replacementMap); } void AddInputAttachmentsToUniforms(const TVector &inputAttachmentVars, std::vector *uniforms) { for (const TVariable *inputVar : inputAttachmentVars) { if (inputVar == nullptr) { continue; } ShaderVariable uniform; uniform.active = true; uniform.staticUse = true; uniform.name.assign(inputVar->name().data(), inputVar->name().length()); uniform.mappedName.assign(uniform.name); uniform.isFragmentInOut = true; uniform.location = inputVar->getType().getLayoutQualifier().inputAttachmentIndex; uniforms->push_back(uniform); } } } // anonymous namespace [[nodiscard]] bool EmulateFramebufferFetch(TCompiler *compiler, TIntermBlock *root, std::vector *uniforms) { // First, check if input attachments are necessary at all. TVector attachmentTypes; InputAttachmentUsageTraverser usageTraverser(compiler->getBuiltInResources().MaxDrawBuffers, &attachmentTypes); root->traverse(&usageTraverser); InputAttachmentIndexUsage indexUsage = usageTraverser.getIndexUsage(); if (!indexUsage.any()) { return true; } const bool usesLastFragData = compiler->getShaderVersion() == 100 || usageTraverser.usesLastFragColorARM(); // Declare the necessary variables for emulation; input attachments to read from and global // variables to hold last frag data. TVector inputAttachmentVars; const TVariable *lastFragData = nullptr; if (!DeclareVariables(compiler, root, indexUsage, usesLastFragData, attachmentTypes, &inputAttachmentVars, &lastFragData)) { return false; } // Then replace references to gl_LastFragData with the global, gl_LastFragColorARM with // global[0], replace inout variables with out equivalents and make sure input attachments // initialize the appropriate variables at the beginning of the shader. if (!ReplaceVariables(compiler, root, inputAttachmentVars, lastFragData)) { return false; } // Add the newly declared variables to the list of uniforms. AddInputAttachmentsToUniforms(inputAttachmentVars, uniforms); return true; } } // namespace sh