// // Copyright 2024 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. // #include "compiler/translator/wgsl/TranslatorWGSL.h" #include #include #include "GLSLANG/ShaderLang.h" #include "common/log_utils.h" #include "common/span.h" #include "compiler/translator/BaseTypes.h" #include "compiler/translator/Common.h" #include "compiler/translator/Diagnostics.h" #include "compiler/translator/ImmutableString.h" #include "compiler/translator/ImmutableStringBuilder.h" #include "compiler/translator/InfoSink.h" #include "compiler/translator/IntermNode.h" #include "compiler/translator/Operator_autogen.h" #include "compiler/translator/OutputTree.h" #include "compiler/translator/StaticType.h" #include "compiler/translator/SymbolUniqueId.h" #include "compiler/translator/Types.h" #include "compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.h" #include "compiler/translator/tree_ops/RewriteArrayOfArrayOfOpaqueUniforms.h" #include "compiler/translator/tree_ops/RewriteStructSamplers.h" #include "compiler/translator/tree_ops/SeparateStructFromUniformDeclarations.h" #include "compiler/translator/tree_util/BuiltIn_autogen.h" #include "compiler/translator/tree_util/FindMain.h" #include "compiler/translator/tree_util/IntermNode_util.h" #include "compiler/translator/tree_util/IntermTraverse.h" #include "compiler/translator/tree_util/RunAtTheEndOfShader.h" #include "compiler/translator/wgsl/OutputUniformBlocks.h" #include "compiler/translator/wgsl/RewritePipelineVariables.h" #include "compiler/translator/wgsl/Utils.h" namespace sh { namespace { constexpr bool kOutputTreeBeforeTranslation = false; constexpr bool kOutputTranslatedShader = false; struct VarDecl { const SymbolType symbolType = SymbolType::Empty; const ImmutableString &symbolName; const TType &type; }; bool IsDefaultUniform(const TType &type) { return type.getQualifier() == EvqUniform && type.getInterfaceBlock() == nullptr && !IsOpaqueType(type.getBasicType()); } // When emitting a list of statements, this determines whether a semicolon follows the statement. bool RequiresSemicolonTerminator(TIntermNode &node) { if (node.getAsBlock()) { return false; } if (node.getAsLoopNode()) { return false; } if (node.getAsSwitchNode()) { return false; } if (node.getAsIfElseNode()) { return false; } if (node.getAsFunctionDefinition()) { return false; } if (node.getAsCaseNode()) { return false; } return true; } // For pretty formatting of the resulting WGSL text. bool NewlinePad(TIntermNode &node) { if (node.getAsFunctionDefinition()) { return true; } if (TIntermDeclaration *declNode = node.getAsDeclarationNode()) { ASSERT(declNode->getChildCount() == 1); TIntermNode &childNode = *declNode->getChildNode(0); if (TIntermSymbol *symbolNode = childNode.getAsSymbolNode()) { const TVariable &var = symbolNode->variable(); return var.getType().isStructSpecifier(); } return false; } return false; } // A traverser that generates WGSL as it walks the AST. class OutputWGSLTraverser : public TIntermTraverser { public: OutputWGSLTraverser(TInfoSinkBase *sink, RewritePipelineVarOutput *rewritePipelineVarOutput, UniformBlockMetadata *uniformBlockMetadata, WGSLGenerationMetadataForUniforms *arrayElementTypesInUniforms); ~OutputWGSLTraverser() override; protected: void visitSymbol(TIntermSymbol *node) override; void visitConstantUnion(TIntermConstantUnion *node) override; bool visitSwizzle(Visit visit, TIntermSwizzle *node) override; bool visitBinary(Visit visit, TIntermBinary *node) override; bool visitUnary(Visit visit, TIntermUnary *node) override; bool visitTernary(Visit visit, TIntermTernary *node) override; bool visitIfElse(Visit visit, TIntermIfElse *node) override; bool visitSwitch(Visit visit, TIntermSwitch *node) override; bool visitCase(Visit visit, TIntermCase *node) override; void visitFunctionPrototype(TIntermFunctionPrototype *node) override; bool visitFunctionDefinition(Visit visit, TIntermFunctionDefinition *node) override; bool visitAggregate(Visit visit, TIntermAggregate *node) override; bool visitBlock(Visit visit, TIntermBlock *node) override; bool visitGlobalQualifierDeclaration(Visit visit, TIntermGlobalQualifierDeclaration *node) override; bool visitDeclaration(Visit visit, TIntermDeclaration *node) override; bool visitLoop(Visit visit, TIntermLoop *node) override; bool visitBranch(Visit visit, TIntermBranch *node) override; void visitPreprocessorDirective(TIntermPreprocessorDirective *node) override; private: struct EmitVariableDeclarationConfig { EmitTypeConfig typeConfig; bool isParameter = false; bool disableStructSpecifier = false; bool needsVar = false; bool isGlobalScope = false; }; void groupedTraverse(TIntermNode &node); void emitNameOf(const VarDecl &decl); void emitBareTypeName(const TType &type); void emitType(const TType &type); void emitSingleConstant(const TConstantUnion *const constUnion); const TConstantUnion *emitConstantUnionArray(const TConstantUnion *const constUnion, const size_t size); const TConstantUnion *emitConstantUnion(const TType &type, const TConstantUnion *constUnionBegin); const TField &getDirectField(const TIntermTyped &fieldsNode, TIntermTyped &indexNode); void emitIndentation(); void emitOpenBrace(); void emitCloseBrace(); bool emitBlock(angle::Span nodes); void emitFunctionSignature(const TFunction &func); void emitFunctionReturn(const TFunction &func); void emitFunctionParameter(const TFunction &func, const TVariable ¶m); void emitStructDeclaration(const TType &type); void emitVariableDeclaration(const VarDecl &decl, const EmitVariableDeclarationConfig &evdConfig); void emitArrayIndex(TIntermTyped &leftNode, TIntermTyped &rightNode); void emitStructIndex(TIntermBinary *binaryNode); void emitStructIndexNoUnwrapping(TIntermBinary *binaryNode); void emitTextureBuiltin(const TOperator op, const TIntermSequence &args); bool emitForLoop(TIntermLoop *); bool emitWhileLoop(TIntermLoop *); bool emulateDoWhileLoop(TIntermLoop *); TInfoSinkBase &mSink; const RewritePipelineVarOutput *mRewritePipelineVarOutput; const UniformBlockMetadata *mUniformBlockMetadata; WGSLGenerationMetadataForUniforms *mWGSLGenerationMetadataForUniforms; int mIndentLevel = -1; int mLastIndentationPos = -1; }; OutputWGSLTraverser::OutputWGSLTraverser( TInfoSinkBase *sink, RewritePipelineVarOutput *rewritePipelineVarOutput, UniformBlockMetadata *uniformBlockMetadata, WGSLGenerationMetadataForUniforms *wgslGenerationMetadataForUniforms) : TIntermTraverser(true, false, false), mSink(*sink), mRewritePipelineVarOutput(rewritePipelineVarOutput), mUniformBlockMetadata(uniformBlockMetadata), mWGSLGenerationMetadataForUniforms(wgslGenerationMetadataForUniforms) {} OutputWGSLTraverser::~OutputWGSLTraverser() = default; void OutputWGSLTraverser::groupedTraverse(TIntermNode &node) { // TODO(anglebug.com/42267100): to make generated code more readable, do not always // emit parentheses like WGSL is some Lisp dialect. const bool emitParens = true; if (emitParens) { mSink << "("; } node.traverse(this); if (emitParens) { mSink << ")"; } } void OutputWGSLTraverser::emitNameOf(const VarDecl &decl) { WriteNameOf(mSink, decl.symbolType, decl.symbolName); } void OutputWGSLTraverser::emitIndentation() { ASSERT(mIndentLevel >= 0); if (mLastIndentationPos == mSink.size()) { return; // Line is already indented. } for (int i = 0; i < mIndentLevel; ++i) { mSink << " "; } mLastIndentationPos = mSink.size(); } void OutputWGSLTraverser::emitOpenBrace() { ASSERT(mIndentLevel >= 0); emitIndentation(); mSink << "{\n"; ++mIndentLevel; } void OutputWGSLTraverser::emitCloseBrace() { ASSERT(mIndentLevel >= 1); --mIndentLevel; emitIndentation(); mSink << "}"; } void OutputWGSLTraverser::visitSymbol(TIntermSymbol *symbolNode) { const TVariable &var = symbolNode->variable(); const TType &type = var.getType(); ASSERT(var.symbolType() != SymbolType::Empty); if (type.getBasicType() == TBasicType::EbtVoid) { UNREACHABLE(); } else { // Accesses of pipeline variables should be rewritten as struct accesses. if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId())) { mSink << kBuiltinInputStructName << "." << var.name(); } else if (mRewritePipelineVarOutput->IsOutputVar(var.uniqueId())) { mSink << kBuiltinOutputStructName << "." << var.name(); } // Accesses of basic uniforms need to be converted to struct accesses. else if (IsDefaultUniform(type)) { mSink << kDefaultUniformBlockVarName << "." << var.name(); } else { WriteNameOf(mSink, var); } if (var.symbolType() == SymbolType::BuiltIn) { ASSERT(mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) || mRewritePipelineVarOutput->IsOutputVar(var.uniqueId()) || var.uniqueId() == BuiltInId::gl_DepthRange); // TODO(anglebug.com/42267100): support gl_DepthRange. // Match the name of the struct field in `mRewritePipelineVarOutput`. mSink << "_"; } } } void OutputWGSLTraverser::emitSingleConstant(const TConstantUnion *const constUnion) { switch (constUnion->getType()) { case TBasicType::EbtBool: { mSink << (constUnion->getBConst() ? "true" : "false"); } break; case TBasicType::EbtFloat: { float value = constUnion->getFConst(); if (std::isnan(value)) { UNIMPLEMENTED(); // TODO(anglebug.com/42267100): this is not a valid constant in WGPU. // You can't even do something like bitcast(0xffffffffu). // The WGSL compiler still complains. I think this is because // WGSL supports implementations compiling with -ffastmath and // therefore nans and infinities are assumed to not exist. // See also https://github.com/gpuweb/gpuweb/issues/3749. mSink << "NAN_INVALID"; } else if (std::isinf(value)) { UNIMPLEMENTED(); // see above. mSink << "INFINITY_INVALID"; } else { mSink << value << "f"; } } break; case TBasicType::EbtInt: { mSink << constUnion->getIConst() << "i"; } break; case TBasicType::EbtUInt: { mSink << constUnion->getUConst() << "u"; } break; default: { UNIMPLEMENTED(); } } } const TConstantUnion *OutputWGSLTraverser::emitConstantUnionArray( const TConstantUnion *const constUnion, const size_t size) { const TConstantUnion *constUnionIterated = constUnion; for (size_t i = 0; i < size; i++, constUnionIterated++) { emitSingleConstant(constUnionIterated); if (i != size - 1) { mSink << ", "; } } return constUnionIterated; } const TConstantUnion *OutputWGSLTraverser::emitConstantUnion(const TType &type, const TConstantUnion *constUnionBegin) { const TConstantUnion *constUnionCurr = constUnionBegin; const TStructure *structure = type.getStruct(); if (structure) { emitType(type); // Structs are constructed with parentheses in WGSL. mSink << "("; // Emit the constructor parameters. Both GLSL and WGSL require there to be the same number // of parameters as struct fields. const TFieldList &fields = structure->fields(); for (size_t i = 0; i < fields.size(); ++i) { const TType *fieldType = fields[i]->type(); constUnionCurr = emitConstantUnion(*fieldType, constUnionCurr); if (i != fields.size() - 1) { mSink << ", "; } } mSink << ")"; } else { size_t size = type.getObjectSize(); // If the type's size is more than 1, the type needs to be written with parantheses. This // applies for vectors, matrices, and arrays. bool writeType = size > 1; if (writeType) { emitType(type); mSink << "("; } constUnionCurr = emitConstantUnionArray(constUnionCurr, size); if (writeType) { mSink << ")"; } } return constUnionCurr; } void OutputWGSLTraverser::visitConstantUnion(TIntermConstantUnion *constValueNode) { emitConstantUnion(constValueNode->getType(), constValueNode->getConstantValue()); } bool OutputWGSLTraverser::visitSwizzle(Visit, TIntermSwizzle *swizzleNode) { groupedTraverse(*swizzleNode->getOperand()); mSink << "." << swizzleNode->getOffsetsAsXYZW(); return false; } const char *GetOperatorString(TOperator op, const TType &resultType, const TType *argType0, const TType *argType1, const TType *argType2) { switch (op) { case TOperator::EOpComma: // WGSL does not have a comma operator or any other way to implement "statement list as // an expression", so nested expressions will have to be pulled out into statements. UNIMPLEMENTED(); return "TODO_operator"; case TOperator::EOpAssign: return "="; case TOperator::EOpInitialize: return "="; // Compound assignments now exist: https://www.w3.org/TR/WGSL/#compound-assignment-sec case TOperator::EOpAddAssign: return "+="; case TOperator::EOpSubAssign: return "-="; case TOperator::EOpMulAssign: return "*="; case TOperator::EOpDivAssign: return "/="; case TOperator::EOpIModAssign: return "%="; case TOperator::EOpBitShiftLeftAssign: return "<<="; case TOperator::EOpBitShiftRightAssign: return ">>="; case TOperator::EOpBitwiseAndAssign: return "&="; case TOperator::EOpBitwiseXorAssign: return "^="; case TOperator::EOpBitwiseOrAssign: return "|="; case TOperator::EOpAdd: return "+"; case TOperator::EOpSub: return "-"; case TOperator::EOpMul: return "*"; case TOperator::EOpDiv: return "/"; // TODO(anglebug.com/42267100): Works different from GLSL for negative numbers. // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=not%20WGSL%3B%20etc.-,Inconsistent%20mod/%25%20operator,-At%20first%20glance // GLSL does `x - y * floor(x/y)`, WGSL does x - y * trunc(x/y). case TOperator::EOpIMod: case TOperator::EOpMod: return "%"; case TOperator::EOpBitShiftLeft: return "<<"; case TOperator::EOpBitShiftRight: return ">>"; case TOperator::EOpBitwiseAnd: return "&"; case TOperator::EOpBitwiseXor: return "^"; case TOperator::EOpBitwiseOr: return "|"; case TOperator::EOpLessThan: return "<"; case TOperator::EOpGreaterThan: return ">"; case TOperator::EOpLessThanEqual: return "<="; case TOperator::EOpGreaterThanEqual: return ">="; // Component-wise comparisons are done with regular infix operators in WGSL: // https://www.w3.org/TR/WGSL/#comparison-expr case TOperator::EOpLessThanComponentWise: return "<"; case TOperator::EOpLessThanEqualComponentWise: return "<="; case TOperator::EOpGreaterThanEqualComponentWise: return ">="; case TOperator::EOpGreaterThanComponentWise: return ">"; case TOperator::EOpLogicalOr: return "||"; // Logical XOR is only applied to boolean expressions so it's the same as "not equals". // Neither short-circuits. case TOperator::EOpLogicalXor: return "!="; case TOperator::EOpLogicalAnd: return "&&"; case TOperator::EOpNegative: return "-"; case TOperator::EOpPositive: if (argType0->isMatrix()) { return ""; } return "+"; case TOperator::EOpLogicalNot: return "!"; // Component-wise not done with normal prefix unary operator in WGSL: // https://www.w3.org/TR/WGSL/#logical-expr case TOperator::EOpNotComponentWise: return "!"; case TOperator::EOpBitwiseNot: return "~"; // TODO(anglebug.com/42267100): increment operations cannot be used as expressions in WGSL. case TOperator::EOpPostIncrement: return "++"; case TOperator::EOpPostDecrement: return "--"; case TOperator::EOpPreIncrement: case TOperator::EOpPreDecrement: // TODO(anglebug.com/42267100): pre increments and decrements do not exist in WGSL. UNIMPLEMENTED(); return "TODO_operator"; case TOperator::EOpVectorTimesScalarAssign: return "*="; case TOperator::EOpVectorTimesMatrixAssign: return "*="; case TOperator::EOpMatrixTimesScalarAssign: return "*="; case TOperator::EOpMatrixTimesMatrixAssign: return "*="; case TOperator::EOpVectorTimesScalar: return "*"; case TOperator::EOpVectorTimesMatrix: return "*"; case TOperator::EOpMatrixTimesVector: return "*"; case TOperator::EOpMatrixTimesScalar: return "*"; case TOperator::EOpMatrixTimesMatrix: return "*"; case TOperator::EOpEqualComponentWise: return "=="; case TOperator::EOpNotEqualComponentWise: return "!="; // TODO(anglebug.com/42267100): structs, matrices, and arrays are not comparable with WGSL's // == or !=. Comparing vectors results in a component-wise comparison returning a boolean // vector, which is different from GLSL (which use equal(vec, vec) for component-wise // comparison) case TOperator::EOpEqual: if ((argType0->isVector() && argType1->isVector()) || (argType0->getStruct() && argType1->getStruct()) || (argType0->isArray() && argType1->isArray()) || (argType0->isMatrix() && argType1->isMatrix())) { UNIMPLEMENTED(); return "TODO_operator"; } return "=="; case TOperator::EOpNotEqual: if ((argType0->isVector() && argType1->isVector()) || (argType0->getStruct() && argType1->getStruct()) || (argType0->isArray() && argType1->isArray()) || (argType0->isMatrix() && argType1->isMatrix())) { UNIMPLEMENTED(); return "TODO_operator"; } return "!="; case TOperator::EOpKill: case TOperator::EOpReturn: case TOperator::EOpBreak: case TOperator::EOpContinue: // These should all be emitted in visitBranch(). UNREACHABLE(); return "UNREACHABLE_operator"; case TOperator::EOpRadians: return "radians"; case TOperator::EOpDegrees: return "degrees"; case TOperator::EOpAtan: return argType1 == nullptr ? "atan" : "atan2"; case TOperator::EOpRefract: return argType0->isVector() ? "refract" : "TODO_operator"; case TOperator::EOpDistance: return "distance"; case TOperator::EOpLength: return "length"; case TOperator::EOpDot: return argType0->isVector() ? "dot" : "*"; case TOperator::EOpNormalize: return argType0->isVector() ? "normalize" : "sign"; case TOperator::EOpFaceforward: return argType0->isVector() ? "faceForward" : "TODO_Operator"; case TOperator::EOpReflect: return argType0->isVector() ? "reflect" : "TODO_Operator"; case TOperator::EOpMatrixCompMult: return "TODO_Operator"; case TOperator::EOpOuterProduct: return "TODO_Operator"; case TOperator::EOpSign: return "sign"; case TOperator::EOpAbs: return "abs"; case TOperator::EOpAll: return "all"; case TOperator::EOpAny: return "any"; case TOperator::EOpSin: return "sin"; case TOperator::EOpCos: return "cos"; case TOperator::EOpTan: return "tan"; case TOperator::EOpAsin: return "asin"; case TOperator::EOpAcos: return "acos"; case TOperator::EOpSinh: return "sinh"; case TOperator::EOpCosh: return "cosh"; case TOperator::EOpTanh: return "tanh"; case TOperator::EOpAsinh: return "asinh"; case TOperator::EOpAcosh: return "acosh"; case TOperator::EOpAtanh: return "atanh"; case TOperator::EOpFma: return "fma"; // TODO(anglebug.com/42267100): Won't accept pow(vec, f32). // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=Similarly%20pow(vec3%3Cf32%3E%2C%20f32)%20works%20in%20GLSL%20but%20not%20WGSL case TOperator::EOpPow: return "pow"; // GLSL's pow excludes negative x case TOperator::EOpExp: return "exp"; case TOperator::EOpExp2: return "exp2"; case TOperator::EOpLog: return "log"; case TOperator::EOpLog2: return "log2"; case TOperator::EOpSqrt: return "sqrt"; case TOperator::EOpFloor: return "floor"; case TOperator::EOpTrunc: return "trunc"; case TOperator::EOpCeil: return "ceil"; case TOperator::EOpFract: return "fract"; case TOperator::EOpMin: return "min"; case TOperator::EOpMax: return "max"; case TOperator::EOpRound: return "round"; // TODO(anglebug.com/42267100): this is wrong and must round away from // zero if there is a tie. This always rounds to the even number. case TOperator::EOpRoundEven: return "round"; // TODO(anglebug.com/42267100): // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=clamp(vec2%3Cf32%3E%2C%20f32%2C%20f32)%20works%20in%20GLSL%20but%20not%20WGSL%3B%20etc. // Need to expand clamp(vec, low : f32, high : f32) -> // clamp(vec, vec(low), vec(high)) case TOperator::EOpClamp: return "clamp"; case TOperator::EOpSaturate: return "saturate"; case TOperator::EOpMix: if (!argType1->isScalar() && argType2 && argType2->getBasicType() == EbtBool) { return "TODO_Operator"; } return "mix"; case TOperator::EOpStep: return "step"; case TOperator::EOpSmoothstep: return "smoothstep"; case TOperator::EOpModf: UNIMPLEMENTED(); // TODO(anglebug.com/42267100): in WGSL this returns a struct, GLSL it // uses a return value and an outparam return "modf"; case TOperator::EOpIsnan: case TOperator::EOpIsinf: UNIMPLEMENTED(); // TODO(anglebug.com/42267100): WGSL does not allow NaNs or infinity. // What to do about shaders that require this? // Implementations are allowed to assume overflow, infinities, and NaNs are not present // at runtime, however. https://www.w3.org/TR/WGSL/#floating-point-evaluation return "TODO_Operator"; case TOperator::EOpLdexp: // TODO(anglebug.com/42267100): won't accept first arg vector, second arg scalar return "ldexp"; case TOperator::EOpFrexp: return "frexp"; // TODO(anglebug.com/42267100): returns a struct case TOperator::EOpInversesqrt: return "inverseSqrt"; case TOperator::EOpCross: return "cross"; // TODO(anglebug.com/42267100): are these the same? dpdxCoarse() vs dpdxFine()? case TOperator::EOpDFdx: return "dpdx"; case TOperator::EOpDFdy: return "dpdy"; case TOperator::EOpFwidth: return "fwidth"; case TOperator::EOpTranspose: return "transpose"; case TOperator::EOpDeterminant: return "determinant"; case TOperator::EOpInverse: return "TODO_Operator"; // No builtin invert(). // https://github.com/gpuweb/gpuweb/issues/4115 // TODO(anglebug.com/42267100): these interpolateAt*() are not builtin case TOperator::EOpInterpolateAtCentroid: return "TODO_Operator"; case TOperator::EOpInterpolateAtSample: return "TODO_Operator"; case TOperator::EOpInterpolateAtOffset: return "TODO_Operator"; case TOperator::EOpInterpolateAtCenter: return "TODO_Operator"; case TOperator::EOpFloatBitsToInt: case TOperator::EOpFloatBitsToUint: case TOperator::EOpIntBitsToFloat: case TOperator::EOpUintBitsToFloat: { #define BITCAST_SCALAR() \ do \ switch (resultType.getBasicType()) \ { \ case TBasicType::EbtInt: \ return "bitcast"; \ case TBasicType::EbtUInt: \ return "bitcast"; \ case TBasicType::EbtFloat: \ return "bitcast"; \ default: \ UNIMPLEMENTED(); \ return "TOperator_TODO"; \ } \ while (false) #define BITCAST_VECTOR(vecSize) \ do \ switch (resultType.getBasicType()) \ { \ case TBasicType::EbtInt: \ return "bitcast>"; \ case TBasicType::EbtUInt: \ return "bitcast>"; \ case TBasicType::EbtFloat: \ return "bitcast>"; \ default: \ UNIMPLEMENTED(); \ return "TOperator_TODO"; \ } \ while (false) if (resultType.isScalar()) { BITCAST_SCALAR(); } else if (resultType.isVector()) { switch (resultType.getNominalSize()) { case 2: BITCAST_VECTOR("2"); case 3: BITCAST_VECTOR("3"); case 4: BITCAST_VECTOR("4"); default: UNREACHABLE(); return nullptr; } } else { UNIMPLEMENTED(); return "TOperator_TODO"; } #undef BITCAST_SCALAR #undef BITCAST_VECTOR } case TOperator::EOpPackUnorm2x16: return "pack2x16unorm"; case TOperator::EOpPackSnorm2x16: return "pack2x16snorm"; case TOperator::EOpPackUnorm4x8: return "pack4x8unorm"; case TOperator::EOpPackSnorm4x8: return "pack4x8snorm"; case TOperator::EOpUnpackUnorm2x16: return "unpack2x16unorm"; case TOperator::EOpUnpackSnorm2x16: return "unpack2x16snorm"; case TOperator::EOpUnpackUnorm4x8: return "unpack4x8unorm"; case TOperator::EOpUnpackSnorm4x8: return "unpack4x8snorm"; case TOperator::EOpPackHalf2x16: return "pack2x16float"; case TOperator::EOpUnpackHalf2x16: return "unpack2x16float"; case TOperator::EOpBarrier: UNREACHABLE(); return "TOperator_TODO"; case TOperator::EOpMemoryBarrier: // TODO(anglebug.com/42267100): does this exist in WGPU? Device-scoped memory barrier? // Maybe storageBarrier()? UNREACHABLE(); return "TOperator_TODO"; case TOperator::EOpGroupMemoryBarrier: return "workgroupBarrier"; case TOperator::EOpMemoryBarrierAtomicCounter: case TOperator::EOpMemoryBarrierBuffer: case TOperator::EOpMemoryBarrierShared: UNREACHABLE(); return "TOperator_TODO"; case TOperator::EOpAtomicAdd: return "atomicAdd"; case TOperator::EOpAtomicMin: return "atomicMin"; case TOperator::EOpAtomicMax: return "atomicMax"; case TOperator::EOpAtomicAnd: return "atomicAnd"; case TOperator::EOpAtomicOr: return "atomicOr"; case TOperator::EOpAtomicXor: return "atomicXor"; case TOperator::EOpAtomicExchange: return "atomicExchange"; case TOperator::EOpAtomicCompSwap: return "atomicCompareExchangeWeak"; // TODO(anglebug.com/42267100): returns a struct. case TOperator::EOpBitfieldExtract: case TOperator::EOpBitfieldInsert: case TOperator::EOpBitfieldReverse: case TOperator::EOpBitCount: case TOperator::EOpFindLSB: case TOperator::EOpFindMSB: case TOperator::EOpUaddCarry: case TOperator::EOpUsubBorrow: case TOperator::EOpUmulExtended: case TOperator::EOpImulExtended: case TOperator::EOpEmitVertex: case TOperator::EOpEndPrimitive: case TOperator::EOpArrayLength: UNIMPLEMENTED(); return "TOperator_TODO"; case TOperator::EOpNull: case TOperator::EOpConstruct: case TOperator::EOpCallFunctionInAST: case TOperator::EOpCallInternalRawFunction: case TOperator::EOpIndexDirect: case TOperator::EOpIndexIndirect: case TOperator::EOpIndexDirectStruct: case TOperator::EOpIndexDirectInterfaceBlock: UNREACHABLE(); return nullptr; default: // Any other built-in function. return nullptr; } } bool IsSymbolicOperator(TOperator op, const TType &resultType, const TType *argType0, const TType *argType1) { const char *operatorString = GetOperatorString(op, resultType, argType0, argType1, nullptr); if (operatorString == nullptr) { return false; } return !std::isalnum(operatorString[0]); } const TField &OutputWGSLTraverser::getDirectField(const TIntermTyped &fieldsNode, TIntermTyped &indexNode) { const TType &fieldsType = fieldsNode.getType(); const TFieldListCollection *fieldListCollection = fieldsType.getStruct(); if (fieldListCollection == nullptr) { fieldListCollection = fieldsType.getInterfaceBlock(); } ASSERT(fieldListCollection); const TIntermConstantUnion *indexNodeAsConstantUnion = indexNode.getAsConstantUnion(); ASSERT(indexNodeAsConstantUnion); const TConstantUnion &index = *indexNodeAsConstantUnion->getConstantValue(); ASSERT(index.getType() == TBasicType::EbtInt); const TFieldList &fieldList = fieldListCollection->fields(); const int indexVal = index.getIConst(); const TField &field = *fieldList[indexVal]; return field; } void OutputWGSLTraverser::emitArrayIndex(TIntermTyped &leftNode, TIntermTyped &rightNode) { TType leftType = leftNode.getType(); // Some arrays within the uniform address space have their element types wrapped in a struct // when generating WGSL, so this unwraps the element (as an optimization of converting the // entire array back to the unwrapped type). bool needsUnwrapping = false; bool isUniformMatrixNeedingConversion = false; TIntermBinary *leftNodeBinary = leftNode.getAsBinaryNode(); if (leftNodeBinary && leftNodeBinary->getOp() == TOperator::EOpIndexDirectStruct) { const TStructure *structure = leftNodeBinary->getLeft()->getType().getStruct(); bool isInUniformAddressSpace = mUniformBlockMetadata->structsInUniformAddressSpace.count(structure->uniqueId().get()); needsUnwrapping = structure && ElementTypeNeedsUniformWrapperStruct(isInUniformAddressSpace, &leftType); isUniformMatrixNeedingConversion = isInUniformAddressSpace && IsMatCx2(&leftType); ASSERT(!needsUnwrapping || !isUniformMatrixNeedingConversion); } // Emit the left side, which should be of type array. if (needsUnwrapping || isUniformMatrixNeedingConversion) { if (isUniformMatrixNeedingConversion) { // If this array index expression is yielding an std140 matCx2 (i.e. // array), just convert the entire expression to a WGSL matCx2, // instead of converting the entire array of std140 matCx2s into an array of WGSL // matCx2s and then indexing into it. TType baseType = leftType; baseType.toArrayBaseType(); mSink << MakeMatCx2ConversionFunctionName(&baseType) << "("; // Make sure the conversion function referenced here is actually generated in the // resulting WGSL. mWGSLGenerationMetadataForUniforms->outputMatCx2Conversion.insert(baseType); } emitStructIndexNoUnwrapping(leftNodeBinary); } else { groupedTraverse(leftNode); } mSink << "["; const TConstantUnion *constIndex = rightNode.getConstantValue(); // If the array index is a constant that we can statically verify is within array // bounds, just emit that constant. if (!leftType.isUnsizedArray() && constIndex != nullptr && constIndex->getType() == EbtInt && constIndex->getIConst() >= 0 && constIndex->getIConst() < static_cast(leftType.isArray() ? leftType.getOutermostArraySize() : leftType.getNominalSize())) { emitSingleConstant(constIndex); } else { // If the array index is not a constant within the bounds of the array, clamp the // index. mSink << "clamp("; groupedTraverse(rightNode); mSink << ", 0, "; // Now find the array size and clamp it. if (leftType.isUnsizedArray()) { // TODO(anglebug.com/42267100): This is a bug to traverse the `leftNode` a // second time if `leftNode` has side effects (and could also have performance // implications). This should be stored in a temporary variable. This might also // be a bug in the MSL shader compiler. mSink << "arrayLength(&"; groupedTraverse(leftNode); mSink << ")"; } else { uint32_t maxSize; if (leftType.isArray()) { maxSize = leftType.getOutermostArraySize() - 1; } else { maxSize = leftType.getNominalSize() - 1; } mSink << maxSize; } // End the clamp() function. mSink << ")"; } // End the array index operation. mSink << "]"; if (needsUnwrapping) { mSink << "." << kWrappedStructFieldName; } else if (isUniformMatrixNeedingConversion) { // Close conversion function call mSink << ")"; } } void OutputWGSLTraverser::emitStructIndex(TIntermBinary *binaryNode) { ASSERT(binaryNode->getOp() == TOperator::EOpIndexDirectStruct); TIntermTyped &leftNode = *binaryNode->getLeft(); const TType *binaryNodeType = &binaryNode->getType(); const TStructure *structure = leftNode.getType().getStruct(); ASSERT(structure); bool isInUniformAddressSpace = mUniformBlockMetadata->structsInUniformAddressSpace.count(structure->uniqueId().get()); bool isUniformMatrixNeedingConversion = isInUniformAddressSpace && IsMatCx2(binaryNodeType); bool needsUnwrapping = ElementTypeNeedsUniformWrapperStruct(isInUniformAddressSpace, binaryNodeType); if (needsUnwrapping) { ASSERT(!isUniformMatrixNeedingConversion); mSink << MakeUnwrappingArrayConversionFunctionName(&binaryNode->getType()) << "("; // Make sure the conversion function referenced here is actually generated in the resulting // WGSL. mWGSLGenerationMetadataForUniforms->arrayElementTypesThatNeedUnwrappingConversions.insert( *binaryNodeType); } else if (isUniformMatrixNeedingConversion) { mSink << MakeMatCx2ConversionFunctionName(binaryNodeType) << "("; // Make sure the conversion function referenced here is actually generated in the resulting // WGSL. mWGSLGenerationMetadataForUniforms->outputMatCx2Conversion.insert(*binaryNodeType); } emitStructIndexNoUnwrapping(binaryNode); if (needsUnwrapping || isUniformMatrixNeedingConversion) { mSink << ")"; } } void OutputWGSLTraverser::emitStructIndexNoUnwrapping(TIntermBinary *binaryNode) { ASSERT(binaryNode->getOp() == TOperator::EOpIndexDirectStruct); TIntermTyped &leftNode = *binaryNode->getLeft(); TIntermTyped &rightNode = *binaryNode->getRight(); groupedTraverse(leftNode); mSink << "."; WriteNameOf(mSink, getDirectField(leftNode, rightNode)); } bool OutputWGSLTraverser::visitBinary(Visit, TIntermBinary *binaryNode) { const TOperator op = binaryNode->getOp(); TIntermTyped &leftNode = *binaryNode->getLeft(); TIntermTyped &rightNode = *binaryNode->getRight(); switch (op) { case TOperator::EOpIndexDirectStruct: case TOperator::EOpIndexDirectInterfaceBlock: emitStructIndex(binaryNode); break; case TOperator::EOpIndexDirect: case TOperator::EOpIndexIndirect: emitArrayIndex(leftNode, rightNode); break; default: { const TType &resultType = binaryNode->getType(); const TType &leftType = leftNode.getType(); const TType &rightType = rightNode.getType(); // x * y, x ^ y, etc. if (IsSymbolicOperator(op, resultType, &leftType, &rightType)) { groupedTraverse(leftNode); if (op != TOperator::EOpComma) { mSink << " "; } mSink << GetOperatorString(op, resultType, &leftType, &rightType, nullptr) << " "; groupedTraverse(rightNode); } // E.g. builtin function calls else { mSink << GetOperatorString(op, resultType, &leftType, &rightType, nullptr) << "("; leftNode.traverse(this); mSink << ", "; rightNode.traverse(this); mSink << ")"; } } } return false; } bool IsPostfix(TOperator op) { switch (op) { case TOperator::EOpPostIncrement: case TOperator::EOpPostDecrement: return true; default: return false; } } bool OutputWGSLTraverser::visitUnary(Visit, TIntermUnary *unaryNode) { const TOperator op = unaryNode->getOp(); const TType &resultType = unaryNode->getType(); TIntermTyped &arg = *unaryNode->getOperand(); const TType &argType = arg.getType(); const char *name = GetOperatorString(op, resultType, &argType, nullptr, nullptr); // Examples: -x, ~x, ~x if (IsSymbolicOperator(op, resultType, &argType, nullptr)) { const bool postfix = IsPostfix(op); if (!postfix) { mSink << name; } groupedTraverse(arg); if (postfix) { mSink << name; } } else { mSink << name << "("; arg.traverse(this); mSink << ")"; } return false; } bool OutputWGSLTraverser::visitTernary(Visit, TIntermTernary *conditionalNode) { // WGSL does not have a ternary. https://github.com/gpuweb/gpuweb/issues/3747 // The select() builtin is not short circuiting. Maybe we can get if () {} else {} as an // expression, which would also solve the comma operator problem. // TODO(anglebug.com/42267100): as mentioned above this is not correct if the operands have side // effects. Even if they don't have side effects it could have performance implications. // It also doesn't work with all types that ternaries do, e.g. arrays or structs. mSink << "select("; groupedTraverse(*conditionalNode->getFalseExpression()); mSink << ", "; groupedTraverse(*conditionalNode->getTrueExpression()); mSink << ", "; groupedTraverse(*conditionalNode->getCondition()); mSink << ")"; return false; } bool OutputWGSLTraverser::visitIfElse(Visit, TIntermIfElse *ifThenElseNode) { TIntermTyped &condNode = *ifThenElseNode->getCondition(); TIntermBlock *thenNode = ifThenElseNode->getTrueBlock(); TIntermBlock *elseNode = ifThenElseNode->getFalseBlock(); mSink << "if ("; condNode.traverse(this); mSink << ")"; if (thenNode) { mSink << "\n"; thenNode->traverse(this); } else { mSink << " {}"; } if (elseNode) { mSink << "\n"; emitIndentation(); mSink << "else\n"; elseNode->traverse(this); } return false; } bool OutputWGSLTraverser::visitSwitch(Visit, TIntermSwitch *switchNode) { TIntermBlock &stmtList = *switchNode->getStatementList(); emitIndentation(); mSink << "switch "; switchNode->getInit()->traverse(this); mSink << "\n"; emitOpenBrace(); // TODO(anglebug.com/42267100): Case statements that fall through need to combined into a single // case statement with multiple labels. const size_t stmtCount = stmtList.getChildCount(); bool inCaseList = false; size_t currStmt = 0; while (currStmt < stmtCount) { TIntermNode &stmtNode = *stmtList.getChildNode(currStmt); TIntermCase *caseNode = stmtNode.getAsCaseNode(); if (caseNode) { if (inCaseList) { mSink << ", "; } else { emitIndentation(); mSink << "case "; inCaseList = true; } caseNode->traverse(this); // Process the next statement. currStmt++; } else { // The current statement is not a case statement, end the current case list and emit all // the code until the next case statement. WGSL requires braces around the case // statement's code. ASSERT(inCaseList); inCaseList = false; mSink << ":\n"; // Count the statements until the next case (or the end of the switch) and emit them as // a block. This assumes that the current statement list will never fallthrough to the // next case statement. size_t nextCaseStmt = currStmt + 1; for (; nextCaseStmt < stmtCount && !stmtList.getChildNode(nextCaseStmt)->getAsCaseNode(); nextCaseStmt++) { } angle::Span stmtListView(&stmtList.getSequence()->at(currStmt), nextCaseStmt - currStmt); emitBlock(stmtListView); mSink << "\n"; // Skip to the next case statement. currStmt = nextCaseStmt; } } emitCloseBrace(); return false; } bool OutputWGSLTraverser::visitCase(Visit, TIntermCase *caseNode) { // "case" will have been emitted in the visitSwitch() override. if (caseNode->hasCondition()) { TIntermTyped *condExpr = caseNode->getCondition(); condExpr->traverse(this); } else { mSink << "default"; } return false; } void OutputWGSLTraverser::emitFunctionReturn(const TFunction &func) { const TType &returnType = func.getReturnType(); if (returnType.getBasicType() == EbtVoid) { return; } mSink << " -> "; emitType(returnType); } // TODO(anglebug.com/42267100): Function overloads are not supported in WGSL, so function names // should either be emitted mangled or overloaded functions should be renamed in the AST as a // pre-pass. As of Apr 2024, WGSL function overloads are "not coming soon" // (https://github.com/gpuweb/gpuweb/issues/876). void OutputWGSLTraverser::emitFunctionSignature(const TFunction &func) { mSink << "fn "; WriteNameOf(mSink, func); mSink << "("; bool emitComma = false; const size_t paramCount = func.getParamCount(); for (size_t i = 0; i < paramCount; ++i) { if (emitComma) { mSink << ", "; } emitComma = true; const TVariable ¶m = *func.getParam(i); emitFunctionParameter(func, param); } mSink << ")"; emitFunctionReturn(func); } void OutputWGSLTraverser::emitFunctionParameter(const TFunction &func, const TVariable ¶m) { // TODO(anglebug.com/42267100): function parameters are immutable and will need to be renamed if // they are mutated. EmitVariableDeclarationConfig evdConfig; evdConfig.isParameter = true; emitVariableDeclaration({param.symbolType(), param.name(), param.getType()}, evdConfig); } void OutputWGSLTraverser::visitFunctionPrototype(TIntermFunctionPrototype *funcProtoNode) { const TFunction &func = *funcProtoNode->getFunction(); emitIndentation(); // TODO(anglebug.com/42267100): output correct signature for main() if main() is declared as a // function prototype, or perhaps just emit nothing. emitFunctionSignature(func); } bool OutputWGSLTraverser::visitFunctionDefinition(Visit, TIntermFunctionDefinition *funcDefNode) { const TFunction &func = *funcDefNode->getFunction(); TIntermBlock &body = *funcDefNode->getBody(); emitIndentation(); emitFunctionSignature(func); mSink << "\n"; body.traverse(this); return false; } void OutputWGSLTraverser::emitTextureBuiltin(const TOperator op, const TIntermSequence &args) { ASSERT(BuiltInGroup::IsTexture(op)); // The index in the GLSL function's argument list of each particular argument, e.g. bias. // `bias`, `lod`, `offset`, and `P` (the coordinates) are the common arguments to most texture // functions. size_t biasIndex = 0; size_t lodIndex = 0; size_t offsetIndex = 0; size_t pIndex = 0; size_t dpdxIndex = 0; size_t dpdyIndex = 0; // TODO(anglebug.com/389145696): These are probably incorrect translations when sampling from // integer or unsigned integer samplers. Using texture() with a usampler // is similar to using texelFetch(), except wrap modes are respected. Possibly, the correct mip // levels are also selected. // The name of the equivalent texture function in WGSL. ImmutableString wgslFunctionName(""); // GLSL stuffs 1, 2, or 3 arguments into a single vector. These represent the swizzles necessary // for extracting each argument from P to pass to the appropriate WGSL function. ImmutableString coordsSwizzle(""); ImmutableString arrayIndexSwizzle(""); ImmutableString depthRefSwizzle(""); // For the projection forms of the texture builtins, the last coordinate will divide the other // three. This is just a swizzle for the last coordinate if the builtin call includes // projection. ImmutableString projectionDivisionSwizzle(""); ImmutableString wgslTextureVarName(""); ImmutableString wgslSamplerVarName(""); constexpr char k2DCoordsSwizzle[] = ".xy"; constexpr char k3DCoordsSwizzle[] = ".xyz"; constexpr char kPossibleElems[] = "xyzw"; // MonomorphizeUnsupportedFunctions() and RewriteStructSamplers() ensure that this is a // reference to the global sampler. TIntermSymbol *samplerNode = args[0]->getAsSymbolNode(); // TODO(anglebug.com/389145696): this will fail if it's an array of samplers, which isn't yet // handled. if (!samplerNode) { UNIMPLEMENTED(); mSink << "TODO_UNHANDLED_TEXTURE_FUNCTION()"; return; } TBasicType samplerType = samplerNode->getType().getBasicType(); ASSERT(IsSampler(samplerType)); bool isProj = false; auto setWgslTextureVarName = [&]() { wgslTextureVarName = BuildConcatenatedImmutableString(kAngleTexturePrefix, samplerNode->getName()); }; auto setWgslSamplerVarName = [&]() { wgslSamplerVarName = BuildConcatenatedImmutableString(kAngleSamplerPrefix, samplerNode->getName()); }; auto setTextureSampleFunctionNameFromBias = [&]() { // TODO(anglebug.com/389145696): these are incorrect translations in vertex shaders, where // they should probably use textureLoad() (and textureDimensions()). if (IsShadowSampler(samplerType)) { if (biasIndex != 0) { // TODO(anglebug.com/389145696): WGSL doesn't support using bias with shadow // samplers. UNIMPLEMENTED(); wgslFunctionName = ImmutableString("TODO_CANNOT_USE_BIAS_WITH_SHADOW_SAMPLER"); } else { wgslFunctionName = ImmutableString("textureSampleCompare"); } } else { if (biasIndex == 0) { wgslFunctionName = ImmutableString("textureSample"); } else { wgslFunctionName = ImmutableString("textureSampleBias"); } } }; switch (op) { case EOpTextureSize: { lodIndex = 1; ASSERT(args.size() == 2); wgslFunctionName = ImmutableString("textureDimensions"); setWgslTextureVarName(); } break; case EOpTexelFetchOffset: case EOpTexelFetch: { pIndex = 1; lodIndex = 2; if (args.size() == 4) { offsetIndex = 3; } ASSERT(args.size() == 3 || args.size() == 4); wgslFunctionName = ImmutableString("textureLoad"); setWgslTextureVarName(); } break; // texture() use to be split into texture2D() and textureCube(). WGSL matches GLSL 3.0 and // combines them. case EOpTextureProj: case EOpTexture2DProj: case EOpTextureProjBias: case EOpTexture2DProjBias: isProj = true; [[fallthrough]]; case EOpTexture: case EOpTexture2D: case EOpTextureCube: case EOpTextureBias: case EOpTexture2DBias: case EOpTextureCubeBias: { pIndex = 1; if (args.size() == 3) { biasIndex = 2; } ASSERT(args.size() == 2 || args.size() == 3); setTextureSampleFunctionNameFromBias(); setWgslTextureVarName(); setWgslSamplerVarName(); } break; case EOpTextureProjLod: case EOpTextureProjLodOffset: case EOpTexture2DProjLodVS: case EOpTexture2DProjLodEXTFS: isProj = true; [[fallthrough]]; case EOpTextureLod: case EOpTexture2DLodVS: case EOpTextureCubeLodVS: case EOpTexture2DLodEXTFS: case EOpTextureCubeLodEXTFS: case EOpTextureLodOffset: { pIndex = 1; lodIndex = 2; if (args.size() == 4) { offsetIndex = 3; } ASSERT(args.size() == 3 || args.size() == 4); if (IsShadowSampler(samplerType)) { // TODO(anglebug.com/389145696): WGSL may not support explicit LOD with shadow // samplers. textureSampleCompareLevel() only uses mip level 0. UNIMPLEMENTED(); wgslFunctionName = ImmutableString("TODO_CANNOT_USE_EXPLICIT_LOD_WITH_SHADOW_SAMPLER"); } else { wgslFunctionName = ImmutableString("textureSampleLevel"); } setWgslTextureVarName(); setWgslSamplerVarName(); } break; case EOpTextureProjOffset: case EOpTextureProjOffsetBias: isProj = true; [[fallthrough]]; case EOpTextureOffset: case EOpTextureOffsetBias: { pIndex = 1; offsetIndex = 2; if (args.size() == 4) { biasIndex = 3; } ASSERT(args.size() == 3 || args.size() == 4); setTextureSampleFunctionNameFromBias(); setWgslTextureVarName(); setWgslSamplerVarName(); } break; case EOpTextureProjGrad: case EOpTextureProjGradOffset: isProj = true; [[fallthrough]]; case EOpTextureGrad: case EOpTextureGradOffset: { pIndex = 1; dpdxIndex = 2; dpdyIndex = 3; if (args.size() == 5) { offsetIndex = 4; } ASSERT(args.size() == 4 || args.size() == 5); if (IsShadowSampler(samplerType)) { // TODO(anglebug.com/389145696): WGSL may not support explicit gradients with shadow // samplers. UNIMPLEMENTED(); wgslFunctionName = ImmutableString("TODO_CANNOT_USE_EXPLICIT_GRAD_WITH_SHADOW_SAMPLER"); } else { wgslFunctionName = ImmutableString("textureSampleGrad"); } setWgslTextureVarName(); setWgslSamplerVarName(); } break; default: UNIMPLEMENTED(); mSink << "TODO_UNHANDLED_TEXTURE_FUNCTION()"; return; } mSink << wgslFunctionName << "("; ASSERT(!wgslTextureVarName.empty()); mSink << wgslTextureVarName; if (!wgslSamplerVarName.empty()) { mSink << ", " << wgslSamplerVarName; // If using a projection division, set the swizzle that extracts the last argument from the // p vector. if (isProj) { ASSERT(pIndex == 1); const uint8_t vecSize = args[pIndex]->getAsTyped()->getNominalSize(); ASSERT(vecSize == 3 || vecSize == 4); projectionDivisionSwizzle = BuildConcatenatedImmutableString('.', kPossibleElems[vecSize - 1]); } // If sampling from an array, set the swizzle that extracts the array layer number from the // p vector. if (IsSampler2DArray(samplerType)) { arrayIndexSwizzle = ImmutableString(".z"); } // If sampling from a shadow samplers, set the swizzle that extracts the D_ref argument from // the p vector. if (IsShadowSampler(samplerType)) { size_t elemIndex = 0; if (IsSampler2D(samplerType)) { elemIndex = 2; } else if (IsSampler2DArray(samplerType) || IsSampler3D(samplerType) || IsSamplerCube(samplerType)) { elemIndex = 3; } depthRefSwizzle = BuildConcatenatedImmutableString('.', kPossibleElems[elemIndex]); } // Finally, set the swizzle for extracting coordinates from the p vector. if (IsSampler2D(samplerType) || IsSampler2DArray(samplerType)) { coordsSwizzle = ImmutableString(k2DCoordsSwizzle); } else if (IsSampler3D(samplerType) || IsSamplerCube(samplerType)) { coordsSwizzle = ImmutableString(k3DCoordsSwizzle); } } // TODO(anglebug.com/389145696): traversing the pArg multiple times is an error if it ever // contains side effects (e.g. a function call). There is also a problem if this traverses // function arguments in a different order, arguments with side effects that effect arguments // that come later may be reordered incorrectly. ESSL specs defined function argument evaluation // as left-to-right. auto traversePArg = [&]() { mSink << "("; ASSERT(pIndex != 0); args[pIndex]->traverse(this); mSink << ")"; }; auto outputProjectionDivisionIfNecessary = [&]() { if (projectionDivisionSwizzle.empty()) { return; } mSink << " / "; traversePArg(); mSink << projectionDivisionSwizzle; }; // The arguments to the WGSL function always appear in a certain (partial) order, so output them // in that order. // // The order is always // - texture // - sampler // - coordinates // - array layer index // - depth_ref, bias, explicit level of detail (never appear together) // - dfdx // - dfdy // - offset // // See the texture builtin functions in the WGSL spec: // https://www.w3.org/TR/WGSL/#texture-builtin-functions // // For example // @must_use fn textureSampleLevel(t: texture_2d_array, // s: sampler, // coords: vec2, // array_index: A, // level: f32, // offset: vec2) -> vec4 if (pIndex != 0) { mSink << ", "; traversePArg(); mSink << coordsSwizzle; outputProjectionDivisionIfNecessary(); } if (!arrayIndexSwizzle.empty()) { mSink << ", "; traversePArg(); mSink << arrayIndexSwizzle; } if (!depthRefSwizzle.empty()) { mSink << ", "; traversePArg(); mSink << depthRefSwizzle; outputProjectionDivisionIfNecessary(); } if (biasIndex != 0) { mSink << ", "; args[biasIndex]->traverse(this); } if (lodIndex != 0) { mSink << ", "; args[lodIndex]->traverse(this); } if (dpdxIndex != 0) { mSink << ", "; args[dpdxIndex]->traverse(this); } if (dpdyIndex != 0) { mSink << ", "; args[dpdyIndex]->traverse(this); } if (offsetIndex != 0) { mSink << ", "; // Both GLSL and WGSL require this to be a const expression. args[offsetIndex]->traverse(this); } mSink << ")"; } bool OutputWGSLTraverser::visitAggregate(Visit, TIntermAggregate *aggregateNode) { const TIntermSequence &args = *aggregateNode->getSequence(); auto emitArgList = [&]() { mSink << "("; bool emitComma = false; for (TIntermNode *arg : args) { if (emitComma) { mSink << ", "; } emitComma = true; arg->traverse(this); } mSink << ")"; }; const TType &retType = aggregateNode->getType(); if (aggregateNode->isConstructor()) { emitType(retType); emitArgList(); return false; } else { const TOperator op = aggregateNode->getOp(); switch (op) { case TOperator::EOpCallFunctionInAST: WriteNameOf(mSink, *aggregateNode->getFunction()); emitArgList(); return false; default: // Do not allow raw function calls, i.e. calls to functions // not present in the AST. ASSERT(op != TOperator::EOpCallInternalRawFunction); auto getArgType = [&](size_t index) -> const TType * { if (index < args.size()) { TIntermTyped *arg = args[index]->getAsTyped(); ASSERT(arg); return &arg->getType(); } return nullptr; }; const TType *argType0 = getArgType(0); const TType *argType1 = getArgType(1); const TType *argType2 = getArgType(2); const char *opName = GetOperatorString(op, retType, argType0, argType1, argType2); if (IsSymbolicOperator(op, retType, argType0, argType1)) { switch (args.size()) { case 1: { TIntermNode &operandNode = *aggregateNode->getChildNode(0); if (IsPostfix(op)) { mSink << opName; groupedTraverse(operandNode); } else { groupedTraverse(operandNode); mSink << opName; } return false; } case 2: { // symbolic operators with 2 args are emitted with infix notation. TIntermNode &leftNode = *aggregateNode->getChildNode(0); TIntermNode &rightNode = *aggregateNode->getChildNode(1); groupedTraverse(leftNode); mSink << " " << opName << " "; groupedTraverse(rightNode); return false; } default: UNREACHABLE(); return false; } } else { // Rewrite the calls to sampler functions. if (BuiltInGroup::IsTexture(op)) { emitTextureBuiltin(op, args); return false; } // If the operator is not symbolic then it is a builtin that uses function call // syntax: builtin(arg1, arg2, ..); mSink << (opName == nullptr ? "TODO_Operator" : opName); emitArgList(); return false; } } } } bool OutputWGSLTraverser::emitBlock(angle::Span nodes) { ASSERT(mIndentLevel >= -1); const bool isGlobalScope = mIndentLevel == -1; if (isGlobalScope) { ++mIndentLevel; } else { emitOpenBrace(); } TIntermNode *prevStmtNode = nullptr; const size_t stmtCount = nodes.size(); for (size_t i = 0; i < stmtCount; ++i) { TIntermNode &stmtNode = *nodes[i]; if (isGlobalScope && prevStmtNode && (NewlinePad(*prevStmtNode) || NewlinePad(stmtNode))) { mSink << "\n"; } const bool isCase = stmtNode.getAsCaseNode(); mIndentLevel -= isCase; emitIndentation(); mIndentLevel += isCase; stmtNode.traverse(this); if (RequiresSemicolonTerminator(stmtNode)) { mSink << ";"; } mSink << "\n"; prevStmtNode = &stmtNode; } if (isGlobalScope) { ASSERT(mIndentLevel == 0); --mIndentLevel; } else { emitCloseBrace(); } return false; } bool OutputWGSLTraverser::visitBlock(Visit, TIntermBlock *blockNode) { return emitBlock( angle::Span(blockNode->getSequence()->data(), blockNode->getSequence()->size())); } bool OutputWGSLTraverser::visitGlobalQualifierDeclaration(Visit, TIntermGlobalQualifierDeclaration *) { return false; } void OutputWGSLTraverser::emitStructDeclaration(const TType &type) { ASSERT(type.getBasicType() == TBasicType::EbtStruct); ASSERT(type.isStructSpecifier()); mSink << "struct "; emitBareTypeName(type); mSink << "\n"; emitOpenBrace(); const TStructure &structure = *type.getStruct(); bool isInUniformAddressSpace = mUniformBlockMetadata->structsInUniformAddressSpace.count(structure.uniqueId().get()) != 0; bool alignTo16InUniformAddressSpace = true; for (const TField *field : structure.fields()) { const TType *fieldType = field->type(); emitIndentation(); // If this struct is used in the uniform address space, it must obey the uniform address // space's layout constaints (https://www.w3.org/TR/WGSL/#address-space-layout-constraints). // WGSL's address space layout constraints nearly match std140, and the places they don't // are handled elsewhere. if (isInUniformAddressSpace) { // Here, the field must be aligned to 16 if: // 1. The field is a struct or array (note that matCx2 is represented as an array of // vec2) // 2. The previous field is a struct // 3. The field is the first in the struct (for convenience). if (field->type()->getStruct() || fieldType->isArray() || IsMatCx2(fieldType)) { alignTo16InUniformAddressSpace = true; } if (alignTo16InUniformAddressSpace) { mSink << "@align(16) "; } // If this field is a struct, the next member should be aligned to 16. alignTo16InUniformAddressSpace = fieldType->getStruct(); // If the field is an array whose stride is not aligned to 16, the element type must be // emitted with a wrapper struct. Record that the wrapper struct needs to be emitted. // Note that if the array element type is already of struct type, it doesn't need // another wrapper struct, it will automatically be aligned to 16 because its first // member is aligned to 16 (implemented above). if (ElementTypeNeedsUniformWrapperStruct(/*inUniformAddressSpace=*/true, fieldType)) { TType innerType = *fieldType; innerType.toArrayElementType(); // Multidimensional arrays not currently supported in uniforms in the WebGPU backend ASSERT(!innerType.isArray()); mWGSLGenerationMetadataForUniforms->arrayElementTypesInUniforms.insert(innerType); } } // TODO(anglebug.com/42267100): emit qualifiers. EmitVariableDeclarationConfig evdConfig; evdConfig.typeConfig.addressSpace = isInUniformAddressSpace ? WgslAddressSpace::Uniform : WgslAddressSpace::NonUniform; evdConfig.disableStructSpecifier = true; emitVariableDeclaration({field->symbolType(), field->name(), *fieldType}, evdConfig); mSink << ",\n"; } emitCloseBrace(); } void OutputWGSLTraverser::emitVariableDeclaration(const VarDecl &decl, const EmitVariableDeclarationConfig &evdConfig) { const TBasicType basicType = decl.type.getBasicType(); if (decl.type.getQualifier() == EvqUniform) { // Uniforms are declared in a pre-pass, and don't need to be outputted here. return; } if (basicType == TBasicType::EbtStruct && decl.type.isStructSpecifier() && !evdConfig.disableStructSpecifier) { // TODO(anglebug.com/42267100): in WGSL structs probably can't be declared in // function parameters or in uniform declarations or in variable declarations, or // anonymously either within other structs or within a variable declaration. Handle // these with the same AST pre-passes as other shader translators. ASSERT(!evdConfig.isParameter); emitStructDeclaration(decl.type); if (decl.symbolType != SymbolType::Empty) { mSink << " "; emitNameOf(decl); } return; } ASSERT(basicType == TBasicType::EbtStruct || decl.symbolType != SymbolType::Empty || evdConfig.isParameter); if (evdConfig.needsVar) { // "const" and "let" probably don't need to be ever emitted because they are more for // readability, and the GLSL compiler constant folds most (all?) the consts anyway. mSink << "var"; // TODO(anglebug.com/42267100): or ? if (evdConfig.isGlobalScope) { if (decl.type.getQualifier() == EvqUniform) { ASSERT(IsOpaqueType(decl.type.getBasicType())); mSink << ""; } else { mSink << ""; } } mSink << " "; } else { ASSERT(!evdConfig.isGlobalScope); } if (decl.symbolType != SymbolType::Empty) { emitNameOf(decl); } mSink << " : "; WriteWgslType(mSink, decl.type, evdConfig.typeConfig); } bool OutputWGSLTraverser::visitDeclaration(Visit, TIntermDeclaration *declNode) { ASSERT(declNode->getChildCount() == 1); TIntermNode &node = *declNode->getChildNode(0); EmitVariableDeclarationConfig evdConfig; evdConfig.needsVar = true; evdConfig.isGlobalScope = mIndentLevel == 0; if (TIntermSymbol *symbolNode = node.getAsSymbolNode()) { const TVariable &var = symbolNode->variable(); if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) || mRewritePipelineVarOutput->IsOutputVar(var.uniqueId())) { // Some variables, like shader inputs/outputs/builtins, are declared in the WGSL source // outside of the traverser. return false; } emitVariableDeclaration({var.symbolType(), var.name(), var.getType()}, evdConfig); } else if (TIntermBinary *initNode = node.getAsBinaryNode()) { ASSERT(initNode->getOp() == TOperator::EOpInitialize); TIntermSymbol *leftSymbolNode = initNode->getLeft()->getAsSymbolNode(); TIntermTyped *valueNode = initNode->getRight()->getAsTyped(); ASSERT(leftSymbolNode && valueNode); const TVariable &var = leftSymbolNode->variable(); if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) || mRewritePipelineVarOutput->IsOutputVar(var.uniqueId())) { // Some variables, like shader inputs/outputs/builtins, are declared in the WGSL source // outside of the traverser. return false; } emitVariableDeclaration({var.symbolType(), var.name(), var.getType()}, evdConfig); mSink << " = "; groupedTraverse(*valueNode); } else { UNREACHABLE(); } return false; } bool OutputWGSLTraverser::visitLoop(Visit, TIntermLoop *loopNode) { const TLoopType loopType = loopNode->getType(); switch (loopType) { case TLoopType::ELoopFor: return emitForLoop(loopNode); case TLoopType::ELoopWhile: return emitWhileLoop(loopNode); case TLoopType::ELoopDoWhile: return emulateDoWhileLoop(loopNode); } } bool OutputWGSLTraverser::emitForLoop(TIntermLoop *loopNode) { ASSERT(loopNode->getType() == TLoopType::ELoopFor); TIntermNode *initNode = loopNode->getInit(); TIntermTyped *condNode = loopNode->getCondition(); TIntermTyped *exprNode = loopNode->getExpression(); mSink << "for ("; if (initNode) { initNode->traverse(this); } else { mSink << " "; } mSink << "; "; if (condNode) { condNode->traverse(this); } mSink << "; "; if (exprNode) { exprNode->traverse(this); } mSink << ")\n"; loopNode->getBody()->traverse(this); return false; } bool OutputWGSLTraverser::emitWhileLoop(TIntermLoop *loopNode) { ASSERT(loopNode->getType() == TLoopType::ELoopWhile); TIntermNode *initNode = loopNode->getInit(); TIntermTyped *condNode = loopNode->getCondition(); TIntermTyped *exprNode = loopNode->getExpression(); ASSERT(condNode); ASSERT(!initNode && !exprNode); emitIndentation(); mSink << "while ("; condNode->traverse(this); mSink << ")\n"; loopNode->getBody()->traverse(this); return false; } bool OutputWGSLTraverser::emulateDoWhileLoop(TIntermLoop *loopNode) { ASSERT(loopNode->getType() == TLoopType::ELoopDoWhile); TIntermNode *initNode = loopNode->getInit(); TIntermTyped *condNode = loopNode->getCondition(); TIntermTyped *exprNode = loopNode->getExpression(); ASSERT(condNode); ASSERT(!initNode && !exprNode); emitIndentation(); // Write an infinite loop. mSink << "loop {\n"; mIndentLevel++; loopNode->getBody()->traverse(this); mSink << "\n"; emitIndentation(); // At the end of the loop, break if the loop condition dos not still hold. mSink << "if (!("; condNode->traverse(this); mSink << ") { break; }\n"; mIndentLevel--; emitIndentation(); mSink << "}"; return false; } bool OutputWGSLTraverser::visitBranch(Visit, TIntermBranch *branchNode) { const TOperator flowOp = branchNode->getFlowOp(); TIntermTyped *exprNode = branchNode->getExpression(); emitIndentation(); switch (flowOp) { case TOperator::EOpKill: { ASSERT(exprNode == nullptr); mSink << "discard"; } break; case TOperator::EOpReturn: { mSink << "return"; if (exprNode) { mSink << " "; exprNode->traverse(this); } } break; case TOperator::EOpBreak: { ASSERT(exprNode == nullptr); mSink << "break"; } break; case TOperator::EOpContinue: { ASSERT(exprNode == nullptr); mSink << "continue"; } break; default: { UNREACHABLE(); } } return false; } void OutputWGSLTraverser::visitPreprocessorDirective(TIntermPreprocessorDirective *node) { // No preprocessor directives expected at this point. UNREACHABLE(); } void OutputWGSLTraverser::emitBareTypeName(const TType &type) { WriteWgslBareTypeName(mSink, type, {}); } void OutputWGSLTraverser::emitType(const TType &type) { WriteWgslType(mSink, type, {}); } } // namespace TranslatorWGSL::TranslatorWGSL(sh::GLenum type, ShShaderSpec spec, ShShaderOutput output) : TCompiler(type, spec, output) {} bool TranslatorWGSL::preTranslateTreeModifications(TIntermBlock *root) { int aggregateTypesUsedForUniforms = 0; for (const auto &uniform : getUniforms()) { if (uniform.isStruct() || uniform.isArrayOfArrays()) { ++aggregateTypesUsedForUniforms; } } // Samplers are legal as function parameters, but samplers within structs or arrays are not // allowed in WGSL // (https://www.w3.org/TR/WGSL/#function-call-expr:~:text=A%20function%20parameter,a%20sampler%20type). // TODO(anglebug.com/389145696): handle arrays of samplers here. // If there are any function calls that take array-of-array of opaque uniform parameters, or // other opaque uniforms that need special handling in WebGPU, monomorphize the functions by // removing said parameters and replacing them in the function body with the call arguments. // // This dramatically simplifies future transformations w.r.t to samplers in structs, array of // arrays of opaque types, atomic counters etc. UnsupportedFunctionArgsBitSet args{UnsupportedFunctionArgs::StructContainingSamplers, UnsupportedFunctionArgs::ArrayOfArrayOfSamplerOrImage, UnsupportedFunctionArgs::AtomicCounter, UnsupportedFunctionArgs::Image}; if (!MonomorphizeUnsupportedFunctions(this, root, &getSymbolTable(), args)) { return false; } if (aggregateTypesUsedForUniforms > 0) { if (!SeparateStructFromUniformDeclarations(this, root, &getSymbolTable())) { return false; } int removedUniformsCount; // Requires MonomorphizeUnsupportedFunctions() to have been run already. if (!RewriteStructSamplers(this, root, &getSymbolTable(), &removedUniformsCount)) { return false; } } // Replace array of array of opaque uniforms with a flattened array. This is run after // MonomorphizeUnsupportedFunctions and RewriteStructSamplers so that it's not possible for an // array of array of opaque type to be partially subscripted and passed to a function. // TODO(anglebug.com/389145696): Even single-level arrays of samplers are not allowed in WGSL. if (!RewriteArrayOfArrayOfOpaqueUniforms(this, root, &getSymbolTable())) { return false; } return true; } bool TranslatorWGSL::translate(TIntermBlock *root, const ShCompileOptions &compileOptions, PerformanceDiagnostics *perfDiagnostics) { if (kOutputTreeBeforeTranslation) { OutputTree(root, getInfoSink().info); std::cout << getInfoSink().info.c_str(); } if (!preTranslateTreeModifications(root)) { return false; } enableValidateNoMoreTransformations(); RewritePipelineVarOutput rewritePipelineVarOutput(getShaderType()); WGSLGenerationMetadataForUniforms wgslGenerationMetadataForUniforms; // WGSL's main() will need to take parameters or return values if any glsl (input/output) // builtin variables are used. if (!GenerateMainFunctionAndIOStructs(*this, *root, rewritePipelineVarOutput)) { return false; } TInfoSinkBase &sink = getInfoSink().obj; // Start writing the output structs that will be referred to by the `traverser`'s output.' if (!rewritePipelineVarOutput.OutputStructs(sink)) { return false; } if (!OutputUniformBlocksAndSamplers(this, root)) { return false; } UniformBlockMetadata uniformBlockMetadata; if (!RecordUniformBlockMetadata(root, uniformBlockMetadata)) { return false; } // Generate the body of the WGSL including the GLSL main() function. TInfoSinkBase traverserOutput; OutputWGSLTraverser traverser(&traverserOutput, &rewritePipelineVarOutput, &uniformBlockMetadata, &wgslGenerationMetadataForUniforms); root->traverse(&traverser); sink << "\n"; OutputUniformWrapperStructsAndConversions(sink, wgslGenerationMetadataForUniforms); // The traverser output needs to be in the code after uniform wrapper structs are emitted above, // since the traverser code references the wrapper struct types. sink << traverserOutput.str(); // Write the actual WGSL main function, wgslMain(), which calls the GLSL main function. if (!rewritePipelineVarOutput.OutputMainFunction(sink)) { return false; } if (kOutputTranslatedShader) { std::cout << sink.str(); } return true; } bool TranslatorWGSL::shouldFlattenPragmaStdglInvariantAll() { // Not neccesary for WGSL transformation. return false; } } // namespace sh