std::string WGSLCodeGenerator::assembleIntrinsicCall()

in src/sksl/codegen/SkSLWGSLCodeGenerator.cpp [3069:3347]


std::string WGSLCodeGenerator::assembleIntrinsicCall(const FunctionCall& call,
                                                     IntrinsicKind kind,
                                                     Precedence parentPrecedence) {
    // Be careful: WGSL 1.0 will reject any intrinsic calls which can be constant-evaluated to
    // infinity or nan with a compile error. If all arguments to an intrinsic are compile-time
    // constants (`all_arguments_constant`), it is safest to copy one argument into a scratch-let so
    // that the call will be seen as runtime-evaluated, which defuses the overflow checks.
    // Don't worry; a competent driver should still optimize it away.

    const ExpressionArray& arguments = call.arguments();
    switch (kind) {
        case k_atan_IntrinsicKind: {
            const char* name = (arguments.size() == 1) ? "atan" : "atan2";
            return this->assembleSimpleIntrinsic(name, call);
        }
        case k_dFdx_IntrinsicKind:
            return this->assembleSimpleIntrinsic("dpdx", call);

        case k_dFdy_IntrinsicKind:
            // TODO(b/294274678): apply RTFlip here
            return this->assembleSimpleIntrinsic("dpdy", call);

        case k_dot_IntrinsicKind: {
            if (arguments[0]->type().isScalar()) {
                return this->assembleBinaryOpIntrinsic(OperatorKind::STAR, call, parentPrecedence);
            }
            return this->assembleSimpleIntrinsic("dot", call);
        }
        case k_equal_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::EQEQ, call, parentPrecedence);

        case k_faceforward_IntrinsicKind: {
            if (arguments[0]->type().isScalar()) {
                // select(-N, N, (I * Nref) < 0)
                std::string N = this->writeNontrivialScratchLet(*arguments[0],
                                                                Precedence::kAssignment);
                return this->writeScratchLet(
                        "select(-" + N + ", " + N + ", " +
                        this->assembleBinaryExpression(*arguments[1],
                                                       OperatorKind::STAR,
                                                       *arguments[2],
                                                       arguments[1]->type(),
                                                       Precedence::kRelational) +
                        " < 0)");
            }
            return this->assembleSimpleIntrinsic("faceForward", call);
        }
        case k_frexp_IntrinsicKind:
            // SkSL frexp is "$genType fract = frexp($genType, out $genIType exp)" whereas WGSL
            // returns a struct with no out param: "let [fract, exp] = frexp($genType)".
            return this->assembleOutAssignedIntrinsic("frexp", "fract", "exp", call);

        case k_greaterThan_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::GT, call, parentPrecedence);

        case k_greaterThanEqual_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::GTEQ, call, parentPrecedence);

        case k_inverse_IntrinsicKind:
            return this->assembleInversePolyfill(call);

        case k_inversesqrt_IntrinsicKind:
            return this->assembleSimpleIntrinsic("inverseSqrt", call);

        case k_lessThan_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::LT, call, parentPrecedence);

        case k_lessThanEqual_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::LTEQ, call, parentPrecedence);

        case k_matrixCompMult_IntrinsicKind: {
            // We use a scratch-let for arg0 to avoid the potential for WGSL overflow. (skia:14385)
            std::string arg0 = all_arguments_constant(arguments)
                            ? this->writeScratchLet(*arguments[0], Precedence::kPostfix)
                            : this->writeNontrivialScratchLet(*arguments[0], Precedence::kPostfix);
            std::string arg1 = this->writeNontrivialScratchLet(*arguments[1], Precedence::kPostfix);
            return this->writeScratchLet(
                    this->assembleComponentwiseMatrixBinary(arguments[0]->type(),
                                                            arguments[1]->type(),
                                                            arg0,
                                                            arg1,
                                                            OperatorKind::STAR));
        }
        case k_mix_IntrinsicKind: {
            const char* name = arguments[2]->type().componentType().isBoolean() ? "select" : "mix";
            return this->assembleVectorizedIntrinsic(name, call);
        }
        case k_mod_IntrinsicKind: {
            // WGSL has no intrinsic equivalent to `mod`. Synthesize `x - y * floor(x / y)`.
            // We can use a scratch-let on one side to dodge WGSL overflow errors.  In practice, I
            // can't find any values of x or y which would overflow, but it can't hurt. (skia:14385)
            std::string arg0 = all_arguments_constant(arguments)
                            ? this->writeScratchLet(*arguments[0], Precedence::kAdditive)
                            : this->writeNontrivialScratchLet(*arguments[0], Precedence::kAdditive);
            std::string arg1 = this->writeNontrivialScratchLet(*arguments[1],
                                                               Precedence::kAdditive);
            return this->writeScratchLet(arg0 + " - " + arg1 + " * floor(" +
                                         arg0 + " / " + arg1 + ")");
        }

        case k_modf_IntrinsicKind:
            // SkSL modf is "$genType fract = modf($genType, out $genType whole)" whereas WGSL
            // returns a struct with no out param: "let [fract, whole] = modf($genType)".
            return this->assembleOutAssignedIntrinsic("modf", "fract", "whole", call);

        case k_normalize_IntrinsicKind: {
            const char* name = arguments[0]->type().isScalar() ? "sign" : "normalize";
            return this->assembleSimpleIntrinsic(name, call);
        }
        case k_not_IntrinsicKind:
            return this->assembleUnaryOpIntrinsic(OperatorKind::LOGICALNOT, call, parentPrecedence);

        case k_notEqual_IntrinsicKind:
            return this->assembleBinaryOpIntrinsic(OperatorKind::NEQ, call, parentPrecedence);

        case k_packHalf2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("pack2x16float", call);

        case k_packSnorm2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("pack2x16snorm", call);

        case k_packSnorm4x8_IntrinsicKind:
            return this->assembleSimpleIntrinsic("pack4x8snorm", call);

        case k_packUnorm2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("pack2x16unorm", call);

        case k_packUnorm4x8_IntrinsicKind:
            return this->assembleSimpleIntrinsic("pack4x8unorm", call);

        case k_reflect_IntrinsicKind:
            if (arguments[0]->type().isScalar()) {
                // I - 2 * N * I * N
                // We can use a scratch-let for N to dodge WGSL overflow errors. (skia:14385)
                std::string I = this->writeNontrivialScratchLet(*arguments[0],
                                                                Precedence::kAdditive);
                std::string N = all_arguments_constant(arguments)
                      ? this->writeScratchLet(*arguments[1], Precedence::kMultiplicative)
                      : this->writeNontrivialScratchLet(*arguments[1], Precedence::kMultiplicative);
                return this->writeScratchLet(String::printf("%s - 2 * %s * %s * %s",
                                                            I.c_str(), N.c_str(),
                                                            I.c_str(), N.c_str()));
            }
            return this->assembleSimpleIntrinsic("reflect", call);

        case k_refract_IntrinsicKind:
            if (arguments[0]->type().isScalar()) {
                // WGSL only implements refract for vectors; rather than reimplementing refract from
                // scratch, we can replace the call with `refract(float2(I,0), float2(N,0), eta).x`.
                std::string I = this->writeNontrivialScratchLet(*arguments[0],
                                                                Precedence::kSequence);
                std::string N = this->writeNontrivialScratchLet(*arguments[1],
                                                                Precedence::kSequence);
                // We can use a scratch-let for Eta to avoid WGSL overflow errors. (skia:14385)
                std::string Eta = all_arguments_constant(arguments)
                      ? this->writeScratchLet(*arguments[2], Precedence::kSequence)
                      : this->writeNontrivialScratchLet(*arguments[2], Precedence::kSequence);
                return this->writeScratchLet(
                        String::printf("refract(vec2<%s>(%s, 0), vec2<%s>(%s, 0), %s).x",
                                       to_wgsl_type(fContext, arguments[0]->type()).c_str(),
                                       I.c_str(),
                                       to_wgsl_type(fContext, arguments[1]->type()).c_str(),
                                       N.c_str(),
                                       Eta.c_str()));
            }
            return this->assembleSimpleIntrinsic("refract", call);

        case k_sample_IntrinsicKind: {
            // Determine if a bias argument was passed in.
            SkASSERT(arguments.size() == 2 || arguments.size() == 3);
            bool callIncludesBias = (arguments.size() == 3);

            if (fProgram.fConfig->fSettings.fSharpenTextures || callIncludesBias) {
                // We need to supply a bias argument; this is a separate intrinsic in WGSL.
                std::string expr = this->assemblePartialSampleCall("textureSampleBias",
                                                                   *arguments[0],
                                                                   *arguments[1]);
                expr += ", ";
                if (callIncludesBias) {
                    expr += this->assembleExpression(*arguments[2], Precedence::kAdditive) +
                            " + ";
                }
                expr += skstd::to_string(fProgram.fConfig->fSettings.fSharpenTextures
                                                 ? kSharpenTexturesBias
                                                 : 0.0f);
                return expr + ')';
            }

            // No bias is necessary, so we can call `textureSample` directly.
            return this->assemblePartialSampleCall("textureSample",
                                                   *arguments[0],
                                                   *arguments[1]) + ')';
        }
        case k_sampleLod_IntrinsicKind: {
            std::string expr = this->assemblePartialSampleCall("textureSampleLevel",
                                                               *arguments[0],
                                                               *arguments[1]);
            expr += ", " + this->assembleExpression(*arguments[2], Precedence::kSequence);
            return expr + ')';
        }
        case k_sampleGrad_IntrinsicKind: {
            std::string expr = this->assemblePartialSampleCall("textureSampleGrad",
                                                               *arguments[0],
                                                               *arguments[1]);
            expr += ", " + this->assembleExpression(*arguments[2], Precedence::kSequence);
            expr += ", " + this->assembleExpression(*arguments[3], Precedence::kSequence);
            return expr + ')';
        }
        case k_textureHeight_IntrinsicKind:
            return this->assembleSimpleIntrinsic("textureDimensions", call) + ".y";

        case k_textureRead_IntrinsicKind: {
            // We need to inject an extra argument for the mip-level. We don't plan on using mipmaps
            // in our storage textures, so we can just pass zero.
            std::string tex = this->assembleExpression(*arguments[0], Precedence::kSequence);
            std::string pos = this->writeScratchLet(*arguments[1], Precedence::kSequence);
            return std::string("textureLoad(") + tex + ", " + pos + ", 0)";
        }
        case k_textureWidth_IntrinsicKind:
            return this->assembleSimpleIntrinsic("textureDimensions", call) + ".x";

        case k_textureWrite_IntrinsicKind:
            return this->assembleSimpleIntrinsic("textureStore", call);

        case k_unpackHalf2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("unpack2x16float", call);

        case k_unpackSnorm2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("unpack2x16snorm", call);

        case k_unpackSnorm4x8_IntrinsicKind:
            return this->assembleSimpleIntrinsic("unpack4x8snorm", call);

        case k_unpackUnorm2x16_IntrinsicKind:
            return this->assembleSimpleIntrinsic("unpack2x16unorm", call);

        case k_unpackUnorm4x8_IntrinsicKind:
            return this->assembleSimpleIntrinsic("unpack4x8unorm", call);

        case k_clamp_IntrinsicKind:
        case k_max_IntrinsicKind:
        case k_min_IntrinsicKind:
        case k_smoothstep_IntrinsicKind:
        case k_step_IntrinsicKind:
            return this->assembleVectorizedIntrinsic(call.function().name(), call);

        case k_abs_IntrinsicKind:
        case k_acos_IntrinsicKind:
        case k_all_IntrinsicKind:
        case k_any_IntrinsicKind:
        case k_asin_IntrinsicKind:
        case k_atomicAdd_IntrinsicKind:
        case k_atomicLoad_IntrinsicKind:
        case k_atomicStore_IntrinsicKind:
        case k_ceil_IntrinsicKind:
        case k_cos_IntrinsicKind:
        case k_cross_IntrinsicKind:
        case k_degrees_IntrinsicKind:
        case k_distance_IntrinsicKind:
        case k_exp_IntrinsicKind:
        case k_exp2_IntrinsicKind:
        case k_floor_IntrinsicKind:
        case k_fract_IntrinsicKind:
        case k_length_IntrinsicKind:
        case k_log_IntrinsicKind:
        case k_log2_IntrinsicKind:
        case k_radians_IntrinsicKind:
        case k_pow_IntrinsicKind:
        case k_saturate_IntrinsicKind:
        case k_sign_IntrinsicKind:
        case k_sin_IntrinsicKind:
        case k_sqrt_IntrinsicKind:
        case k_storageBarrier_IntrinsicKind:
        case k_tan_IntrinsicKind:
        case k_workgroupBarrier_IntrinsicKind:
        default:
            return this->assembleSimpleIntrinsic(call.function().name(), call);
    }
}