//
// Copyright 2020 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 <cctype>

#include "compiler/translator/InfoSink.h"
#include "compiler/translator/Symbol.h"
#include "compiler/translator/TranslatorMetalDirect/AstHelpers.h"
#include "compiler/translator/TranslatorMetalDirect/ConstantNames.h"
#include "compiler/translator/TranslatorMetalDirect/Name.h"
#include "compiler/translator/TranslatorMetalDirect/ProgramPrelude.h"
#include "compiler/translator/tree_util/IntermTraverse.h"

using namespace sh;

////////////////////////////////////////////////////////////////////////////////

namespace
{

class ProgramPrelude : public TIntermTraverser
{
    using LineTag       = unsigned;
    using FuncEmitter   = void (*)(ProgramPrelude &, const TFunction &);
    using FuncToEmitter = std::map<Name, FuncEmitter>;

  public:
    ProgramPrelude(TInfoSinkBase &out, const ProgramPreludeConfig &ppc)
        : TIntermTraverser(true, false, false), mOut(out)
    {
        ALWAYS_INLINE();
        int_clamp();
        if (ppc.hasStructEq)
        {
            equalVector();
            equalMatrix();
        }

        switch (ppc.shaderType)
        {
            case MetalShaderType::None:
                ASSERT(0 && "ppc.shaderType should not be ShaderTypeNone");
                break;
            case MetalShaderType::Vertex:
                transform_feedback_guard();
                break;
            case MetalShaderType::Fragment:
                writeSampleMask();
                break;
            case MetalShaderType::Compute:
                ASSERT(0 && "compute shaders not currently supported");
                break;
            default:
                break;
        }

#if 1
        mOut << "#define ANGLE_tensor metal::array\n";
        mOut << "#pragma clang diagnostic ignored \"-Wunused-value\"\n";
#else
        tensor();
#endif
    }

  private:
    bool emitGuard(LineTag lineTag)
    {
        if (mEmitted.find(lineTag) != mEmitted.end())
        {
            return false;
        }
        mEmitted.insert(lineTag);
        return true;
    }

    static FuncToEmitter BuildFuncToEmitter();

    void visitOperator(TOperator op,
                       const TFunction *func,
                       const TType *argType0,
                       const TType *argType1 = nullptr);
    void visitVariable(const Name &name, const TType &type);
    void visitVariable(const TVariable &var);
    void visitStructure(const TStructure &s);

    bool visitBinary(Visit, TIntermBinary *node) override;
    bool visitUnary(Visit, TIntermUnary *node) override;
    bool visitAggregate(Visit, TIntermAggregate *node) override;
    bool visitDeclaration(Visit, TIntermDeclaration *node) override;
    void visitSymbol(TIntermSymbol *node) override;

  private:
    void ALWAYS_INLINE();

    void include_metal_atomic();
    void include_metal_common();
    void include_metal_geometric();
    void include_metal_graphics();
    void include_metal_math();
    void include_metal_matrix();
    void include_metal_pack();
    void include_metal_relational();

    void transform_feedback_guard();

    void enable_if();
    void scalar_of();
    void is_scalar();
    void is_vector();
    void is_matrix();
    void addressof();
    void distance();
    void length();
    void dot();
    void normalize();
    void faceforward();
    void reflect();
    void refract();
    void degrees();
    void radians();
    void mod();
    void postIncrementMatrix();
    void preIncrementMatrix();
    void postDecrementMatrix();
    void preDecrementMatrix();
    void negateMatrix();
    void matmulAssign();
    void atan();
    void int_clamp();
    void addMatrixScalarAssign();
    void subMatrixScalarAssign();
    void addMatrixScalar();
    void subMatrixScalar();
    void divMatrixScalar();
    void divMatrixScalarFast();
    void divMatrixScalarAssign();
    void divMatrixScalarAssignFast();
    void tensor();
    void componentWiseDivide();
    void componentWiseDivideAssign();
    void componentWiseMultiply();
    void outerProduct();
    void inverse2();
    void inverse3();
    void inverse4();
    void equalVector();
    void equalMatrix();
    void notEqualVector();
    void notEqualStruct();
    void notEqualStructArray();
    void notEqualMatrix();
    void equalArray();
    void equalStructArray();
    void notEqualArray();
    void sign();
    void pack_half_2x16();
    void unpack_half_2x16();
    void vectorElemRef();
    void swizzleRef();
    void out();
    void inout();
    void flattenArray();
    void castVector();
    void castMatrix();
    void functionConstants();
    void gradient();
    void writeSampleMask();
    void textureEnv();
    void texelFetch();
    void texelFetchOffset();
    void texture();
    void texture_generic_float2();
    void texture_generic_float2_float();
    void texture_generic_float3();
    void texture_generic_float3_float();
    void texture_depth2d_float3();
    void texture_depth2d_float3_float();
    void texture_depth2darray_float4();
    void texture_depth2darray_float4_float();
    void texture_depthcube_float4();
    void texture_depthcube_float4_float();
    void texture_texture2darray_float3();
    void texture_texture2darray_float3_float();
    void texture_texture2darray_float4();
    void texture_texture2darray_float4_float();
    void texture1DLod();
    void texture1DProj();
    void texture1DProjLod();
    void texture2D();
    void texture2DLod();
    void texture2DProj();
    void texture2DProjLod();
    void texture2DRect();
    void texture2DRectProj();
    void texture3DLod();
    void texture3DProj();
    void texture3DProjLod();
    void textureCube();
    void textureCubeLod();
    void textureCubeProj();
    void textureCubeProjLod();
    void textureGrad();
    void textureGrad_generic_floatN_floatN_floatN();
    void textureGrad_generic_float3_float2_float2();
    void textureGrad_generic_float4_float2_float2();
    void textureGrad_depth2d_float3_float2_float2();
    void textureGrad_depth2darray_float4_float2_float2();
    void textureGrad_depthcube_float4_float3_float3();
    void textureGrad_texturecube_float3_float3_float3();
    void textureGradOffset();
    void textureGradOffset_generic_floatN_floatN_floatN_intN();
    void textureGradOffset_generic_float3_float2_float2_int2();
    void textureGradOffset_generic_float4_float2_float2_int2();
    void textureGradOffset_depth2d_float3_float2_float2_int2();
    void textureGradOffset_depth2darray_float4_float2_float2_int2();
    void textureGradOffset_depthcube_float4_float3_float3_int3();
    void textureGradOffset_texturecube_float3_float3_float3_int3();
    void textureLod();
    void textureLod_generic_float2();
    void textureLod_generic_float3();
    void textureLod_depth2d_float3();
    void textureLod_texture2darray_float3();
    void textureLod_texture2darray_float4();
    void textureLodOffset();
    void textureOffset();
    void textureProj();
    void textureProjGrad();
    void textureProjGrad_generic_float3_float2_float2();
    void textureProjGrad_generic_float4_float2_float2();
    void textureProjGrad_depth2d_float4_float2_float2();
    void textureProjGrad_texture3d_float4_float3_float3();
    void textureProjGradOffset();
    void textureProjGradOffset_generic_float3_float2_float2_int2();
    void textureProjGradOffset_generic_float4_float2_float2_int2();
    void textureProjGradOffset_depth2d_float4_float2_float2_int2();
    void textureProjGradOffset_texture3d_float4_float3_float3_int3();
    void textureProjLod();
    void textureProjLod_generic_float3();
    void textureProjLod_generic_float4();
    void textureProjLod_depth2d_float4();
    void textureProjLod_texture3d_float4();
    void textureProjLodOffset();
    void textureProjOffset();
    void textureSize();

  private:
    TInfoSinkBase &mOut;
    std::unordered_set<LineTag> mEmitted;
    std::unordered_set<const TSymbol *> mHandled;
    const FuncToEmitter mFuncToEmitter = BuildFuncToEmitter();
};

}  // anonymous namespace

////////////////////////////////////////////////////////////////////////////////

#define PROGRAM_PRELUDE_INCLUDE(header)             \
    void ProgramPrelude::include_##header()         \
    {                                               \
        if (emitGuard(__LINE__))                    \
        {                                           \
            mOut << ("#include <" #header ">\n\n"); \
        }                                           \
    }

#define PROGRAM_PRELUDE_DECLARE(name, code, ...)                \
    void ProgramPrelude::name()                                 \
    {                                                           \
        ASSERT(code[0] == '\n');                                \
        if (emitGuard(__LINE__))                                \
        {                                                       \
            __VA_ARGS__; /* dependencies */                     \
            mOut << (static_cast<const char *>(code "\n") + 1); \
        }                                                       \
    }

////////////////////////////////////////////////////////////////////////////////

PROGRAM_PRELUDE_INCLUDE(metal_atomic)
PROGRAM_PRELUDE_INCLUDE(metal_common)
PROGRAM_PRELUDE_INCLUDE(metal_geometric)
PROGRAM_PRELUDE_INCLUDE(metal_graphics)
PROGRAM_PRELUDE_INCLUDE(metal_math)
PROGRAM_PRELUDE_INCLUDE(metal_matrix)
PROGRAM_PRELUDE_INCLUDE(metal_pack)
PROGRAM_PRELUDE_INCLUDE(metal_relational)

PROGRAM_PRELUDE_DECLARE(transform_feedback_guard, R"(
#if TRANSFORM_FEEDBACK_ENABLED
    #define __VERTEX_OUT(args) void
#else
    #define __VERTEX_OUT(args) args
#endif
)")

PROGRAM_PRELUDE_DECLARE(ALWAYS_INLINE, R"(
#define ANGLE_ALWAYS_INLINE __attribute__((always_inline))
)")

PROGRAM_PRELUDE_DECLARE(enable_if, R"(
template <bool B, typename T = void>
struct ANGLE_enable_if {};
template <typename T>
struct ANGLE_enable_if<true, T>
{
    using type = T;
};
template <bool B>
using ANGLE_enable_if_t = typename ANGLE_enable_if<B>::type;
)")

PROGRAM_PRELUDE_DECLARE(scalar_of, R"(
template <typename T>
struct ANGLE_scalar_of
{
    using type = T;
};
template <typename T>
using ANGLE_scalar_of_t = typename ANGLE_scalar_of<T>::type;
)")

PROGRAM_PRELUDE_DECLARE(is_scalar, R"(
template <typename T>
struct ANGLE_is_scalar {};
#define ANGLE_DEFINE_SCALAR(scalar) \
    template <> struct ANGLE_is_scalar<scalar> { enum { value = true }; }
ANGLE_DEFINE_SCALAR(bool);
ANGLE_DEFINE_SCALAR(char);
ANGLE_DEFINE_SCALAR(short);
ANGLE_DEFINE_SCALAR(int);
ANGLE_DEFINE_SCALAR(long);
ANGLE_DEFINE_SCALAR(uchar);
ANGLE_DEFINE_SCALAR(ushort);
ANGLE_DEFINE_SCALAR(uint);
ANGLE_DEFINE_SCALAR(ulong);
ANGLE_DEFINE_SCALAR(half);
ANGLE_DEFINE_SCALAR(float);
)")

PROGRAM_PRELUDE_DECLARE(is_vector,
                        R"(
template <typename T>
struct ANGLE_is_vector
{
    enum { value = false };
};
#define ANGLE_DEFINE_VECTOR(scalar) \
    template <> struct ANGLE_is_vector<metal::scalar ## 2> { enum { value = true }; }; \
    template <> struct ANGLE_is_vector<metal::scalar ## 3> { enum { value = true }; }; \
    template <> struct ANGLE_is_vector<metal::scalar ## 4> { enum { value = true }; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 2> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 3> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 4> { using type = scalar; }
ANGLE_DEFINE_VECTOR(bool);
ANGLE_DEFINE_VECTOR(char);
ANGLE_DEFINE_VECTOR(short);
ANGLE_DEFINE_VECTOR(int);
ANGLE_DEFINE_VECTOR(long);
ANGLE_DEFINE_VECTOR(uchar);
ANGLE_DEFINE_VECTOR(ushort);
ANGLE_DEFINE_VECTOR(uint);
ANGLE_DEFINE_VECTOR(ulong);
ANGLE_DEFINE_VECTOR(half);
ANGLE_DEFINE_VECTOR(float);
)",
                        scalar_of())

PROGRAM_PRELUDE_DECLARE(is_matrix,
                        R"(
template <typename T>
struct ANGLE_is_matrix
{
    enum { value = false };
};
#define ANGLE_DEFINE_MATRIX(scalar) \
    template <> struct ANGLE_is_matrix<metal::scalar ## 2x2> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 2x3> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 2x4> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 3x2> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 3x3> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 3x4> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 4x2> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 4x3> { enum { value = true }; }; \
    template <> struct ANGLE_is_matrix<metal::scalar ## 4x4> { enum { value = true }; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 2x2> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 2x3> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 2x4> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 3x2> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 3x3> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 3x4> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 4x2> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 4x3> { using type = scalar; }; \
    template <> struct ANGLE_scalar_of<metal::scalar ## 4x4> { using type = scalar; }
ANGLE_DEFINE_MATRIX(half);
ANGLE_DEFINE_MATRIX(float);
)",
                        scalar_of())

PROGRAM_PRELUDE_DECLARE(addressof,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE thread T * ANGLE_addressof(thread T &ref)
{
    return &ref;
}
)")

PROGRAM_PRELUDE_DECLARE(distance,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_distance_impl
{
    static ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> exec(T x, T y)
    {
        return metal::distance(x, y);
    }
};
template <typename T>
struct ANGLE_distance_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T x, T y)
    {
        return metal::abs(x - y);
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> ANGLE_distance(T x, T y)
{
    return ANGLE_distance_impl<T>::exec(x, y);
};
)",
                        include_metal_geometric(),
                        include_metal_math(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix())

PROGRAM_PRELUDE_DECLARE(length,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_length_impl
{
    static ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> exec(T x)
    {
        return metal::length(x);
    }
};
template <typename T>
struct ANGLE_length_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T x)
    {
        return metal::abs(x);
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> ANGLE_length(T x)
{
    return ANGLE_length_impl<T>::exec(x);
};
)",
                        include_metal_geometric(),
                        include_metal_math(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix())

PROGRAM_PRELUDE_DECLARE(dot,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_dot_impl
{
    static ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> exec(T x, T y)
    {
        return metal::dot(x, y);
    }
};
template <typename T>
struct ANGLE_dot_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T x, T y)
    {
        return x * y;
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE ANGLE_scalar_of_t<T> ANGLE_dot(T x, T y)
{
    return ANGLE_dot_impl<T>::exec(x, y);
};
)",
                        include_metal_geometric(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix())

PROGRAM_PRELUDE_DECLARE(normalize,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_normalize_impl
{
    static ANGLE_ALWAYS_INLINE T exec(T x)
    {
        return metal::normalize(x);
    }
};
template <typename T>
struct ANGLE_normalize_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T x)
    {
        return ANGLE_sign(x);
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_normalize(T x)
{
    return ANGLE_normalize_impl<T>::exec(x);
};
)",
                        include_metal_common(),
                        include_metal_geometric(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix(),
                        sign())

PROGRAM_PRELUDE_DECLARE(faceforward,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_faceforward_impl
{
    static ANGLE_ALWAYS_INLINE T exec(T n, T i, T nref)
    {
        return metal::faceforward(n, i, nref);
    }
};
template <typename T>
struct ANGLE_faceforward_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T n, T i, T nref)
    {
        return ANGLE_dot(nref, i) < T(0) ? n : -n;
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_faceforward(T n, T i, T nref)
{
    return ANGLE_faceforward_impl<T>::exec(n, i, nref);
};
)",
                        include_metal_geometric(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix(),
                        dot())

PROGRAM_PRELUDE_DECLARE(reflect,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_reflect_impl
{
    static ANGLE_ALWAYS_INLINE T exec(T i, T n)
    {
        return metal::reflect(i, n);
    }
};
template <typename T>
struct ANGLE_reflect_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T i, T n)
    {
        return i - T(2) * ANGLE_dot(n, i) * n;
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_reflect(T i, T n)
{
    return ANGLE_reflect_impl<T>::exec(i, n);
};
)",
                        include_metal_geometric(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix(),
                        dot())

PROGRAM_PRELUDE_DECLARE(refract,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_refract_impl
{
    static ANGLE_ALWAYS_INLINE T exec(T i, T n, ANGLE_scalar_of_t<T> eta)
    {
        return metal::refract(i, n, eta);
    }
};
template <typename T>
struct ANGLE_refract_impl<T, ANGLE_enable_if_t<ANGLE_is_scalar<T>::value>>
{
    static ANGLE_ALWAYS_INLINE T exec(T i, T n, T eta)
    {
        auto dotNI = n * i;
        auto k = T(1) - eta * eta * (T(1) - dotNI * dotNI);
        if (k < T(0))
        {
            return T(0);
        }
        else
        {
            return eta * i - (eta * dotNI + metal::sqrt(k)) * n;
        }
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_refract(T i, T n, ANGLE_scalar_of_t<T> eta)
{
    return ANGLE_refract_impl<T>::exec(i, n, eta);
};
)",
                        include_metal_math(),
                        include_metal_geometric(),
                        enable_if(),
                        is_scalar(),
                        is_vector(),
                        is_matrix())

PROGRAM_PRELUDE_DECLARE(sign,
                        R"(
template <typename T, typename Enable = void>
struct ANGLE_sign_impl
{
    static ANGLE_ALWAYS_INLINE T exec(T x)
    {
        return metal::sign(x);
    }
};
template <>
struct ANGLE_sign_impl<int>
{
    static ANGLE_ALWAYS_INLINE int exec(int x)
    {
        return (0 < x) - (x < 0);
    }
};
template <int N>
struct ANGLE_sign_impl<metal::vec<int, N>>
{
    static ANGLE_ALWAYS_INLINE metal::vec<int, N> exec(metal::vec<int, N> x)
    {
        metal::vec<int, N> s;
        for (int i = 0; i < N; ++i)
        {
            s[i] = ANGLE_sign_impl<int>::exec(x[i]);
        }
        return s;
    }
};
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_sign(T x)
{
    return ANGLE_sign_impl<T>::exec(x);
};
)",
                        include_metal_common())

PROGRAM_PRELUDE_DECLARE(int_clamp,
                        R"(
ANGLE_ALWAYS_INLINE int ANGLE_int_clamp(int value, int minValue, int maxValue)
{
    return ((value < minValue) ?  minValue : ((value > maxValue) ? maxValue : value));
};
)")

PROGRAM_PRELUDE_DECLARE(atan,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_atan(T yOverX)
{
    return metal::atan(yOverX);
}
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_atan(T y, T x)
{
    return metal::atan2(y, x);
}
)",
                        include_metal_math())

PROGRAM_PRELUDE_DECLARE(degrees, R"(
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_degrees(T x)
{
    return static_cast<T>(57.29577951308232) * x;
}
)")

PROGRAM_PRELUDE_DECLARE(radians, R"(
template <typename T>
ANGLE_ALWAYS_INLINE T ANGLE_radians(T x)
{
    return static_cast<T>(1.7453292519943295e-2) * x;
}
)")

PROGRAM_PRELUDE_DECLARE(mod,
                        R"(
template <typename X, typename Y>
ANGLE_ALWAYS_INLINE X ANGLE_mod(X x, Y y)
{
    return x - y * metal::floor(x / y);
}
)",
                        include_metal_math())

PROGRAM_PRELUDE_DECLARE(pack_half_2x16,
                        R"(
ANGLE_ALWAYS_INLINE uint ANGLE_pack_half_2x16(float2 v)
{
    return as_type<uint>(half2(v));
}
)", )

PROGRAM_PRELUDE_DECLARE(unpack_half_2x16,
                        R"(
ANGLE_ALWAYS_INLINE float2 ANGLE_unpack_half_2x16(uint x)
{
    return float2(as_type<half2>(x));
}
)", )

PROGRAM_PRELUDE_DECLARE(matmulAssign, R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator*=(thread metal::matrix<T, Cols, Rows> &a, metal::matrix<T, Cols, Cols> b)
{
    a = a * b;
    return a;
}
)")

PROGRAM_PRELUDE_DECLARE(postIncrementMatrix,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator++(thread metal::matrix<T, Cols, Rows> &a, int)
{
    auto b = a;
    a += T(1);
    return b;
}
)",
                        addMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(preIncrementMatrix,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator++(thread metal::matrix<T, Cols, Rows> &a)
{
    a += T(1);
    return a;
}
)",
                        addMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(postDecrementMatrix,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator--(thread metal::matrix<T, Cols, Rows> &a, int)
{
    auto b = a;
    a -= T(1);
    return b;
}
)",
                        subMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(preDecrementMatrix,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator--(thread metal::matrix<T, Cols, Rows> &a)
{
    a -= T(1);
    return a;
}
)",
                        subMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(negateMatrix,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator-(metal::matrix<T, Cols, Rows> m)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        thread auto &mCol = m[col];
        mCol = -mCol;
    }
    return m;
}
)", )

PROGRAM_PRELUDE_DECLARE(addMatrixScalarAssign, R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator+=(thread metal::matrix<T, Cols, Rows> &m, T x)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        m[col] += x;
    }
    return m;
}
)")

PROGRAM_PRELUDE_DECLARE(addMatrixScalar,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator+(metal::matrix<T, Cols, Rows> m, T x)
{
    m += x;
    return m;
}
)",
                        addMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(subMatrixScalarAssign,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator-=(thread metal::matrix<T, Cols, Rows> &m, T x)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        m[col] -= x;
    }
    return m;
}
)", )

PROGRAM_PRELUDE_DECLARE(subMatrixScalar,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator-(metal::matrix<T, Cols, Rows> m, T x)
{
    m -= x;
    return m;
}
)",
                        subMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(divMatrixScalarAssignFast,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator/=(thread metal::matrix<T, Cols, Rows> &m, T x)
{
    x = T(1) / x;
    for (size_t col = 0; col < Cols; ++col)
    {
        m[col] *= x;
    }
    return m;
}
)", )

PROGRAM_PRELUDE_DECLARE(divMatrixScalarAssign,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator/=(thread metal::matrix<T, Cols, Rows> &m, T x)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        m[col] /= x;
    }
    return m;
}
)", )

PROGRAM_PRELUDE_DECLARE(divMatrixScalarFast,
                        R"(
#if __METAL_VERSION__ <= 220
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator/(metal::matrix<T, Cols, Rows> m, T x)
{
    m /= x;
    return m;
}
#endif
)",
                        divMatrixScalarAssignFast())

PROGRAM_PRELUDE_DECLARE(divMatrixScalar,
                        R"(
#if __METAL_VERSION__ <= 220
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator/(metal::matrix<T, Cols, Rows> m, T x)
{
    m /= x;
    return m;
}
#endif
)",
                        divMatrixScalarAssign())

PROGRAM_PRELUDE_DECLARE(componentWiseDivide, R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> operator/(metal::matrix<T, Cols, Rows> a, metal::matrix<T, Cols, Rows> b)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        a[col] /= b[col];
    }
    return a;
}
)")

PROGRAM_PRELUDE_DECLARE(componentWiseDivideAssign,
                        R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE thread metal::matrix<T, Cols, Rows> &operator/=(thread metal::matrix<T, Cols, Rows> &a, metal::matrix<T, Cols, Rows> b)
{
    a = a / b;
    return a;
}
)",
                        componentWiseDivide())

PROGRAM_PRELUDE_DECLARE(componentWiseMultiply, R"(
template <typename T, int Cols, int Rows>
ANGLE_ALWAYS_INLINE metal::matrix<T, Cols, Rows> ANGLE_componentWiseMultiply(metal::matrix<T, Cols, Rows> a, metal::matrix<T, Cols, Rows> b)
{
    for (size_t col = 0; col < Cols; ++col)
    {
        a[col] *= b[col];
    }
    return a;
}
)")

PROGRAM_PRELUDE_DECLARE(outerProduct, R"(
template <typename T, int M, int N>
ANGLE_ALWAYS_INLINE metal::matrix<T, N, M> ANGLE_outerProduct(metal::vec<T, M> u, metal::vec<T, N> v)
{
    metal::matrix<T, N, M> o;
    for (size_t n = 0; n < N; ++n)
    {
        o[n] = u * v[n];
    }
    return o;
}
)")

PROGRAM_PRELUDE_DECLARE(inverse2, R"(
template <typename T>
ANGLE_ALWAYS_INLINE metal::matrix<T, 2, 2> ANGLE_inverse(metal::matrix<T, 2, 2> m)
{
    metal::matrix<T, 2, 2> adj;
    adj[0][0] =  m[1][1];
    adj[0][1] = -m[0][1];
    adj[1][0] = -m[1][0];
    adj[1][1] =  m[0][0];
    T det = (adj[0][0] * m[0][0]) + (adj[0][1] * m[1][0]);
    return adj * (T(1) / det);
}
)")

PROGRAM_PRELUDE_DECLARE(inverse3, R"(
template <typename T>
ANGLE_ALWAYS_INLINE metal::matrix<T, 3, 3> ANGLE_inverse(metal::matrix<T, 3, 3> m)
{
    T a = m[1][1] * m[2][2] - m[2][1] * m[1][2];
    T b = m[1][0] * m[2][2];
    T c = m[1][2] * m[2][0];
    T d = m[1][0] * m[2][1];
    T det = m[0][0] * (a) -
            m[0][1] * (b - c) +
            m[0][2] * (d - m[1][1] * m[2][0]);
    det = T(1) / det;
    metal::matrix<T, 3, 3> minv;
    minv[0][0] = (a) * det;
    minv[0][1] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * det;
    minv[0][2] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * det;
    minv[1][0] = (c - b) * det;
    minv[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * det;
    minv[1][2] = (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * det;
    minv[2][0] = (d - m[2][0] * m[1][1]) * det;
    minv[2][1] = (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * det;
    minv[2][2] = (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * det;
    return minv;
}
)")

PROGRAM_PRELUDE_DECLARE(inverse4, R"(
template <typename T>
ANGLE_ALWAYS_INLINE metal::matrix<T, 4, 4> ANGLE_inverse(metal::matrix<T, 4, 4> m)
{
    T A2323 = m[2][2] * m[3][3] - m[2][3] * m[3][2];
    T A1323 = m[2][1] * m[3][3] - m[2][3] * m[3][1];
    T A1223 = m[2][1] * m[3][2] - m[2][2] * m[3][1];
    T A0323 = m[2][0] * m[3][3] - m[2][3] * m[3][0];
    T A0223 = m[2][0] * m[3][2] - m[2][2] * m[3][0];
    T A0123 = m[2][0] * m[3][1] - m[2][1] * m[3][0];
    T A2313 = m[1][2] * m[3][3] - m[1][3] * m[3][2];
    T A1313 = m[1][1] * m[3][3] - m[1][3] * m[3][1];
    T A1213 = m[1][1] * m[3][2] - m[1][2] * m[3][1];
    T A2312 = m[1][2] * m[2][3] - m[1][3] * m[2][2];
    T A1312 = m[1][1] * m[2][3] - m[1][3] * m[2][1];
    T A1212 = m[1][1] * m[2][2] - m[1][2] * m[2][1];
    T A0313 = m[1][0] * m[3][3] - m[1][3] * m[3][0];
    T A0213 = m[1][0] * m[3][2] - m[1][2] * m[3][0];
    T A0312 = m[1][0] * m[2][3] - m[1][3] * m[2][0];
    T A0212 = m[1][0] * m[2][2] - m[1][2] * m[2][0];
    T A0113 = m[1][0] * m[3][1] - m[1][1] * m[3][0];
    T A0112 = m[1][0] * m[2][1] - m[1][1] * m[2][0];
    T a = m[1][1] * A2323 - m[1][2] * A1323 + m[1][3] * A1223;
    T b = m[1][0] * A2323 - m[1][2] * A0323 + m[1][3] * A0223;
    T c = m[1][0] * A1323 - m[1][1] * A0323 + m[1][3] * A0123;
    T d = m[1][0] * A1223 - m[1][1] * A0223 + m[1][2] * A0123;
    T det = m[0][0] * ( a )
          - m[0][1] * ( b )
          + m[0][2] * ( c )
          - m[0][3] * ( d );
    det = T(1) / det;
    metal::matrix<T, 4, 4> im;
    im[0][0] = det *   ( a );
    im[0][1] = det * - ( m[0][1] * A2323 - m[0][2] * A1323 + m[0][3] * A1223 );
    im[0][2] = det *   ( m[0][1] * A2313 - m[0][2] * A1313 + m[0][3] * A1213 );
    im[0][3] = det * - ( m[0][1] * A2312 - m[0][2] * A1312 + m[0][3] * A1212 );
    im[1][0] = det * - ( b );
    im[1][1] = det *   ( m[0][0] * A2323 - m[0][2] * A0323 + m[0][3] * A0223 );
    im[1][2] = det * - ( m[0][0] * A2313 - m[0][2] * A0313 + m[0][3] * A0213 );
    im[1][3] = det *   ( m[0][0] * A2312 - m[0][2] * A0312 + m[0][3] * A0212 );
    im[2][0] = det *   ( c );
    im[2][1] = det * - ( m[0][0] * A1323 - m[0][1] * A0323 + m[0][3] * A0123 );
    im[2][2] = det *   ( m[0][0] * A1313 - m[0][1] * A0313 + m[0][3] * A0113 );
    im[2][3] = det * - ( m[0][0] * A1312 - m[0][1] * A0312 + m[0][3] * A0112 );
    im[3][0] = det * - ( d );
    im[3][1] = det *   ( m[0][0] * A1223 - m[0][1] * A0223 + m[0][2] * A0123 );
    im[3][2] = det * - ( m[0][0] * A1213 - m[0][1] * A0213 + m[0][2] * A0113 );
    im[3][3] = det *   ( m[0][0] * A1212 - m[0][1] * A0212 + m[0][2] * A0112 );
    return im;
}
)")

PROGRAM_PRELUDE_DECLARE(equalArray,
                        R"(
template <typename T, size_t N>
ANGLE_ALWAYS_INLINE bool ANGLE_equal(metal::array<T, N> u, metal::array<T, N> v)
{
    for(size_t i = 0; i < N; i++)
        if (u[i] != v[i]) return false;
    return true;
}
)")

PROGRAM_PRELUDE_DECLARE(equalStructArray,
                        R"(
template <typename T, size_t N>
ANGLE_ALWAYS_INLINE bool ANGLE_equalStructArray(metal::array<T, N> u, metal::array<T, N> v)
{
    for(size_t i = 0; i < N; i++)
    {
        if (ANGLE_equal(u[i], v[i]) == false)
            return false;
    }
    return true;
}
)")

PROGRAM_PRELUDE_DECLARE(notEqualArray,
                        R"(
template <typename T, size_t N>
ANGLE_ALWAYS_INLINE bool ANGLE_notEqual(metal::array<T, N> u, metal::array<T, N> v)
{
    return !ANGLE_equal(u,v);
}
)",
                        equalArray())

PROGRAM_PRELUDE_DECLARE(equalVector,
                        R"(
template <typename T, int N>
ANGLE_ALWAYS_INLINE bool ANGLE_equal(metal::vec<T, N> u, metal::vec<T, N> v)
{
    return metal::all(u == v);
}
)",
                        include_metal_math())

PROGRAM_PRELUDE_DECLARE(equalMatrix,
                        R"(
template <typename T, int C, int R>
ANGLE_ALWAYS_INLINE bool ANGLE_equal(metal::matrix<T, C, R> a, metal::matrix<T, C, R> b)
{
    for (int c = 0; c < C; ++c)
    {
        if (!ANGLE_equal(a[c], b[c]))
        {
            return false;
        }
    }
    return true;
}
)",
                        equalVector())

PROGRAM_PRELUDE_DECLARE(notEqualMatrix,
                        R"(
template <typename T, int C, int R>
ANGLE_ALWAYS_INLINE bool ANGLE_notEqual(metal::matrix<T, C, R> u, metal::matrix<T, C, R> v)
{
    return !ANGLE_equal(u, v);
}
)",
                        equalMatrix())

PROGRAM_PRELUDE_DECLARE(notEqualVector,
                        R"(
template <typename T, int N>
ANGLE_ALWAYS_INLINE bool ANGLE_notEqual(metal::vec<T, N> u, metal::vec<T, N> v)
{
    return !ANGLE_equal(u, v);
}
)",
                        equalVector())

PROGRAM_PRELUDE_DECLARE(notEqualStruct,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE bool ANGLE_notEqualStruct(thread const T &a, thread const T &b)
{
    return !ANGLE_equal(a, b);
}
)",
                        equalVector(),
                        equalMatrix())

PROGRAM_PRELUDE_DECLARE(notEqualStructArray,
                        R"(
template <typename T, size_t N>
ANGLE_ALWAYS_INLINE bool ANGLE_notEqualStructArray(metal::array<T, N> u, metal::array<T, N> v)
{
    for(size_t i = 0; i < N; i++)
    {
        if (ANGLE_notEqualStruct(u[i], v[i]))
            return true;
    }
    return false;
}
)",
                        notEqualStruct())

PROGRAM_PRELUDE_DECLARE(vectorElemRef, R"(
template <typename T, int N>
struct ANGLE_VectorElemRef
{
    thread metal::vec<T, N> &mVec;
    T mRef;
    const int mIndex;
    ~ANGLE_VectorElemRef() { mVec[mIndex] = mRef; }
    ANGLE_VectorElemRef(thread metal::vec<T, N> &vec, int index)
        : mVec(vec), mRef(vec[index]), mIndex(index)
    {}
    operator thread T &() { return mRef; }
};
template <typename T, int N>
ANGLE_ALWAYS_INLINE ANGLE_VectorElemRef<T, N> ANGLE_elem_ref(thread metal::vec<T, N> &vec, int index)
{
    return ANGLE_VectorElemRef<T, N>(vec, index);
}
)")

PROGRAM_PRELUDE_DECLARE(swizzleRef,
                        R"(
template <typename T, int VN, int SN>
struct ANGLE_SwizzleRef
{
    thread metal::vec<T, VN> &mVec;
    metal::vec<T, SN> mRef;
    int mIndices[SN];
    ~ANGLE_SwizzleRef()
    {
        for (int i = 0; i < SN; ++i)
        {
            const int j = mIndices[i];
            mVec[j] = mRef[i];
        }
    }
    ANGLE_SwizzleRef(thread metal::vec<T, VN> &vec, thread const int *indices)
        : mVec(vec)
    {
        for (int i = 0; i < SN; ++i)
        {
            const int j = indices[i];
            mIndices[i] = j;
            mRef[i] = mVec[j];
        }
    }
    operator thread metal::vec<T, SN> &() { return mRef; }
};
template <typename T, int N>
ANGLE_ALWAYS_INLINE ANGLE_VectorElemRef<T, N> ANGLE_swizzle_ref(thread metal::vec<T, N> &vec, int i0)
{
    const int is[] = { i0 };
    return ANGLE_VectorElemRef<T, N>(vec, is);
}
template <typename T, int N>
ANGLE_ALWAYS_INLINE ANGLE_SwizzleRef<T, N, 2> ANGLE_swizzle_ref(thread metal::vec<T, N> &vec, int i0, int i1)
{
    const int is[] = { i0, i1 };
    return ANGLE_SwizzleRef<T, N, 2>(vec, is);
}
template <typename T, int N>
ANGLE_ALWAYS_INLINE ANGLE_SwizzleRef<T, N, 3> ANGLE_swizzle_ref(thread metal::vec<T, N> &vec, int i0, int i1, int i2)
{
    const int is[] = { i0, i1, i2 };
    return ANGLE_SwizzleRef<T, N, 3>(vec, is);
}
template <typename T, int N>
ANGLE_ALWAYS_INLINE ANGLE_SwizzleRef<T, N, 4> ANGLE_swizzle_ref(thread metal::vec<T, N> &vec, int i0, int i1, int i2, int i3)
{
    const int is[] = { i0, i1, i2, i3 };
    return ANGLE_SwizzleRef<T, N, 4>(vec, is);
}
)",
                        vectorElemRef())

PROGRAM_PRELUDE_DECLARE(out, R"(
template <typename T>
struct ANGLE_Out
{
    T mTemp;
    thread T &mDest;
    ~ANGLE_Out() { mDest = mTemp; }
    ANGLE_Out(thread T &dest)
        : mTemp(dest), mDest(dest)
    {}
    operator thread T &() { return mTemp; }
};
template <typename T>
ANGLE_ALWAYS_INLINE ANGLE_Out<T> ANGLE_out(thread T &dest)
{
    return ANGLE_Out<T>(dest);
}
)")

PROGRAM_PRELUDE_DECLARE(inout, R"(
template <typename T>
struct ANGLE_InOut
{
    T mTemp;
    thread T &mDest;
    ~ANGLE_InOut() { mDest = mTemp; }
    ANGLE_InOut(thread T &dest)
        : mTemp(dest), mDest(dest)
    {}
    operator thread T &() { return mTemp; }
};
template <typename T>
ANGLE_ALWAYS_INLINE ANGLE_InOut<T> ANGLE_inout(thread T &dest)
{
    return ANGLE_InOut<T>(dest);
}
)")

PROGRAM_PRELUDE_DECLARE(flattenArray, R"(
template <typename T>
struct ANGLE_flatten_impl
{
    static ANGLE_ALWAYS_INLINE thread T *exec(thread T &x)
    {
        return &x;
    }
};
template <typename T, size_t N>
struct ANGLE_flatten_impl<metal::array<T, N>>
{
    static ANGLE_ALWAYS_INLINE auto exec(thread metal::array<T, N> &arr) -> T
    {
        return ANGLE_flatten_impl<T>::exec(arr[0]);
    }
};
template <typename T, size_t N>
ANGLE_ALWAYS_INLINE auto ANGLE_flatten(thread metal::array<T, N> &arr) -> T
{
    return ANGLE_flatten_impl<T>::exec(arr[0]);
}
)")

PROGRAM_PRELUDE_DECLARE(castVector, R"(
template <typename T, int N1, int N2>
struct ANGLE_castVector {};
template <typename T, int N>
struct ANGLE_castVector<T, N, N>
{
    static ANGLE_ALWAYS_INLINE metal::vec<T, N> exec(thread metal::vec<T, N> const &v)
    {
        return v;
    }
};
template <typename T>
struct ANGLE_castVector<T, 2, 3>
{
    static ANGLE_ALWAYS_INLINE metal::vec<T, 2> exec(thread metal::vec<T, 3> const &v)
    {
        return v.xy;
    }
};
template <typename T>
struct ANGLE_castVector<T, 2, 4>
{
    static ANGLE_ALWAYS_INLINE metal::vec<T, 2> exec(thread metal::vec<T, 4> const &v)
    {
        return v.xy;
    }
};
template <typename T>
struct ANGLE_castVector<T, 3, 4>
{
    static ANGLE_ALWAYS_INLINE metal::vec<T, 3> exec(thread metal::vec<T, 4> const &v)
    {
        return as_type<metal::vec<T, 3>>(v);
    }
};
template <int N1, int N2, typename T>
ANGLE_ALWAYS_INLINE metal::vec<T, N1> ANGLE_cast(thread metal::vec<T, N2> const &v)
{
    return ANGLE_castVector<T, N1, N2>::exec(v);
}
)")

PROGRAM_PRELUDE_DECLARE(castMatrix,
                        R"(
template <typename T, int C1, int R1, int C2, int R2, typename Enable = void>
struct ANGLE_castMatrix
{
    static ANGLE_ALWAYS_INLINE metal::matrix<T, C1, R1> exec(thread metal::matrix<T, C2, R2> const &m2)
    {
        metal::matrix<T, C1, R1> m1;
        const int MinC = C1 <= C2 ? C1 : C2;
        const int MinR = R1 <= R2 ? R1 : R2;
        for (int c = 0; c < MinC; ++c)
        {
            for (int r = 0; r < MinR; ++r)
            {
                m1[c][r] = m2[c][r];
            }
            for (int r = R2; r < R1; ++r)
            {
                m1[c][r] = c == r ? T(1) : T(0);
            }
        }
        for (int c = C2; c < C1; ++c)
        {
            for (int r = 0; r < R1; ++r)
            {
                m1[c][r] = c == r ? T(1) : T(0);
            }
        }
        return m1;
    }
};
template <typename T, int C1, int R1, int C2, int R2>
struct ANGLE_castMatrix<T, C1, R1, C2, R2, ANGLE_enable_if_t<(C1 <= C2 && R1 <= R2)>>
{
    static ANGLE_ALWAYS_INLINE metal::matrix<T, C1, R1> exec(thread metal::matrix<T, C2, R2> const &m2)
    {
        metal::matrix<T, C1, R1> m1;
        for (size_t c = 0; c < C1; ++c)
        {
            m1[c] = ANGLE_cast<R1>(m2[c]);
        }
        return m1;
    }
};
template <int C1, int R1, int C2, int R2, typename T>
ANGLE_ALWAYS_INLINE metal::matrix<T, C1, R1> ANGLE_cast(thread metal::matrix<T, C2, R2> const &m)
{
    return ANGLE_castMatrix<T, C1, R1, C2, R2>::exec(m);
};
)",
                        enable_if(),
                        castVector())

PROGRAM_PRELUDE_DECLARE(tensor, R"(
template <typename T, size_t... DS>
struct ANGLE_tensor_traits;
template <typename T, size_t D>
struct ANGLE_tensor_traits<T, D>
{
    enum : size_t { outer_dim = D };
    using inner_type = T;
    using outer_type = inner_type[D];
};
template <typename T, size_t D, size_t... DS>
struct ANGLE_tensor_traits<T, D, DS...>
{
    enum : size_t { outer_dim = D };
    using inner_type = typename ANGLE_tensor_traits<T, DS...>::outer_type;
    using outer_type = inner_type[D];
};
template <size_t D, typename value_type_, typename inner_type_>
struct ANGLE_tensor_impl
{
    enum : size_t { outer_dim = D };
    using value_type = value_type_;
    using inner_type = inner_type_;
    using outer_type = inner_type[D];
    outer_type _data;
    ANGLE_ALWAYS_INLINE size_t size() const { return outer_dim; }
    ANGLE_ALWAYS_INLINE inner_type &operator[](size_t i) { return _data[i]; }
    ANGLE_ALWAYS_INLINE const inner_type &operator[](size_t i) const { return _data[i]; }
};
template <typename T, size_t... DS>
using ANGLE_tensor = ANGLE_tensor_impl<
    ANGLE_tensor_traits<T, DS...>::outer_dim,
    T,
    typename ANGLE_tensor_traits<T, DS...>::inner_type>;
)")

PROGRAM_PRELUDE_DECLARE(gradient,
                        R"(
template <int N>
struct ANGLE_gradient_traits;
template <>
struct ANGLE_gradient_traits<2> { using type = metal::gradient2d; };
template <>
struct ANGLE_gradient_traits<3> { using type = metal::gradient3d; };

template <int N>
using ANGLE_gradient = typename ANGLE_gradient_traits<N>::type;
)")

PROGRAM_PRELUDE_DECLARE(writeSampleMask,
                        R"(
ANGLE_ALWAYS_INLINE void ANGLE_writeSampleMask(const uint mask,
                                               thread uint& gl_SampleMask)
{
    if (ANGLE_CoverageMaskEnabled)
    {
        gl_SampleMask = as_type<int>(mask);
    }
}
)",
                        functionConstants())

PROGRAM_PRELUDE_DECLARE(textureEnv,
                        R"(
template <typename T>
struct ANGLE_TextureEnv
{
    thread T *texture;
    thread metal::sampler *sampler;
};
)")

PROGRAM_PRELUDE_DECLARE(functionConstants,
                        R"(
#define ANGLE_SAMPLE_COMPARE_GRADIENT_INDEX 0
#define ANGLE_SAMPLE_COMPARE_LOD_INDEX      1
#define ANGLE_RASTERIZATION_DISCARD_INDEX   2
#define ANGLE_COVERAGE_MASK_ENABLED_INDEX   3

constant bool ANGLE_UseSampleCompareGradient [[function_constant(ANGLE_SAMPLE_COMPARE_GRADIENT_INDEX)]];
constant bool ANGLE_UseSampleCompareLod      [[function_constant(ANGLE_SAMPLE_COMPARE_LOD_INDEX)]];
constant bool ANGLE_RasterizationDiscard     [[function_constant(ANGLE_RASTERIZATION_DISCARD_INDEX)]];
constant bool ANGLE_CoverageMaskEnabled      [[function_constant(ANGLE_COVERAGE_MASK_ENABLED_INDEX)]];
)")

PROGRAM_PRELUDE_DECLARE(texelFetch,
                        R"(
#define ANGLE_texelFetch(env, ...) ANGLE_texelFetch_impl(*env.texture, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetch_impl(
    thread Texture &texture,
    metal::int2 const coord,
    uint level)
{
    return texture.read(uint2(coord), level);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetch_impl(
    thread Texture &texture,
    metal::int3 const coord,
    uint level)
{
    return texture.read(uint3(coord), level);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetch_impl(
    thread metal::texture2d_array<T> &texture,
    metal::int3 const coord,
    uint level)
{
    return texture.read(uint2(coord.xy), uint(coord.z), level);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texelFetchOffset,
                        R"(
#define ANGLE_texelFetchOffset(env, ...) ANGLE_texelFetchOffset_impl(*env.texture, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetchOffset_impl(
    thread Texture &texture,
    metal::int2 const coord,
    uint level,
    metal::int2 const offset)
{
    return texture.read(uint2(coord + offset), level);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetchOffset_impl(
    thread Texture &texture,
    metal::int3 const coord,
    uint level,
    metal::int3 const offset)
{
    return texture.read(uint3(coord + offset), level);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texelFetchOffset_impl(
    thread metal::texture2d_array<T> &texture,
    metal::int3 const coord,
    uint level,
    metal::int2 const offset)
{
    return texture.read(uint2(coord.xy + offset), uint(coord.z), level);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture,
                        R"(
#define ANGLE_texture(env, ...) ANGLE_texture_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture_generic_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord)
{
    return texture.sample(sampler, coord);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_generic_float2_float,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_generic_float3,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample(sampler, coord);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_generic_float3_float,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depth2d_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample_compare(sampler, coord.xy, coord.z);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depth2d_float3_float,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias)
{
    return texture.sample_compare(sampler, coord.xy, coord.z, metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depth2darray_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depth2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord)
{
    return texture.sample_compare(sampler, coord.xy, uint(metal::round(coord.z)), coord.w);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depth2darray_float4_float,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depth2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float compare)
{
    return texture.sample_compare(sampler, coord.xyz, uint(metal::round(coord.w)), compare);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depthcube_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depthcube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord)
{
    return texture.sample_compare(sampler, coord.xyz, coord.w);
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_depthcube_float4_float,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::depthcube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias)
{
    return texture.sample_compare(sampler, coord.xyz, coord.w, metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_texture2darray_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_texture2darray_float3_float,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_texture2darray_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord)
{
    return texture.sample(sampler, coord.xyz, uint(metal::round(coord.w)));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture_texture2darray_float4_float,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_texture_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias)
{
    return texture.sample(sampler, coord.xyz, uint(metal::round(coord.w)), metal::bias(bias));
}
)",
                        texture())

PROGRAM_PRELUDE_DECLARE(texture1DLod,
                        R"(
#define ANGLE_texture1DLod(env, ...) ANGLE_texture1DLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture1DLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    float const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture1DProj,
                        R"(
#define ANGLE_texture1DProj(env, ...) ANGLE_texture1DProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture1DProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.x/coord.y, metal::bias(bias));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture1DProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.x/coord.w, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture1DProjLod,
                        R"(
#define ANGLE_texture1DProjLod(env, ...) ANGLE_texture1DProjLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture1DProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float level)
{
    return texture.sample(sampler, coord.x/coord.y, metal::level(level));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture1DProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.x/coord.w, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2D,
                        R"(
#define ANGLE_texture2D(env, ...) ANGLE_texture2D_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2D_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord)
{
    return texture.sample(sampler, coord);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2D_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2D_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample(sampler, coord);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2D_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2DRect,
                        R"(
#define ANGLE_texture2DRect(env, ...) ANGLE_texture2DRect_impl(*env.texture, *env.sampler, __VA_ARGS__)
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DRect_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord)
{
    return texture.sample(sampler, coord);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2DLod,
                        R"(
#define ANGLE_texture2DLod(env, ...) ANGLE_texture2DLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2DProj,
                        R"(
#define ANGLE_texture2DProj(env, ...) ANGLE_texture2DProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::bias(bias));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2DRectProj,
                        R"(
#define ANGLE_texture2DRectProj(env, ...) ANGLE_texture2DRectProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DRectProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample(sampler, coord.xy/coord.z);
}
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DRectProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord)
{
    return texture.sample(sampler, coord.xy/coord.w);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture2DProjLod,
                        R"(
#define ANGLE_texture2DProjLod(env, ...) ANGLE_texture2DProjLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::level(level));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture2DProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture3DLod,
                        R"(
#define ANGLE_texture3DLod(env, ...) ANGLE_texture3DLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture3DLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture3DProj,
                        R"(
#define ANGLE_texture3DProj(env, ...) ANGLE_texture3DProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture3DProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(texture3DProjLod,
                        R"(
#define ANGLE_texture3DProjLod(env, ...) ANGLE_texture3DProjLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_texture3DProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureCube,
                        R"(
#define ANGLE_textureCube(env, ...) ANGLE_textureCube_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureCube_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord)
{
    return texture.sample(sampler, coord);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureCube_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureCubeLod,
                        R"(
#define ANGLE_textureCubeLod(env, ...) ANGLE_textureCubeLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureCubeLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureCubeProj,
                        R"(
#define ANGLE_textureCubeProj(env, ...) ANGLE_textureCubeProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureCubeProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureCubeProjLod,
                        R"(
#define ANGLE_textureCubeProjLod(env, ...) ANGLE_textureCubeProjLod_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureCubeProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::level(level));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureGrad,
                        R"(
#define ANGLE_textureGrad(env, ...) ANGLE_textureGrad_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureGrad_generic_floatN_floatN_floatN,
                        R"(
template <typename Texture, int N>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::vec<float, N> const coord,
    metal::vec<float, N> const dPdx,
    metal::vec<float, N> const dPdy)
{
    return texture.sample(sampler, coord, ANGLE_gradient<N>(dPdx, dPdy));
}
)",
                        gradient(),
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_generic_float3_float2_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy));
}
)",
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_generic_float4_float2_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy));
}
)",
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_depth2d_float3_float2_float2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, coord.z, metal::gradient2d(dPdx, dPdy)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, metal::gradient2d(dPdx, dPdy)) > coord.z);
    }
}
)",
                        functionConstants(),
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_depth2darray_float4_float2_float2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread metal::depth2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, uint(metal::round(coord.z)), coord.w, metal::gradient2d(dPdx, dPdy)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy)) > coord.w);
    }
}
)",
                        functionConstants(),
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_depthcube_float4_float3_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread metal::depthcube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xyz, coord.w, metal::gradientcube(dPdx, dPdy)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xyz, metal::gradientcube(dPdx, dPdy)) > coord.w);
    }
}
)",
                        functionConstants(),
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGrad_texturecube_float3_float3_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGrad_impl(
    thread metal::texturecube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy)
{
    return texture.sample(sampler, coord, metal::gradientcube(dPdx, dPdy));
}
)",
                        textureGrad())

PROGRAM_PRELUDE_DECLARE(textureGradOffset,
                        R"(
#define ANGLE_textureGradOffset(env, ...) ANGLE_textureGradOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_generic_floatN_floatN_floatN_intN,
                        R"(
template <typename Texture, int N>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::vec<float, N> const coord,
    metal::vec<float, N> const dPdx,
    metal::vec<float, N> const dPdy,
    metal::vec<int, N> const offset)
{
    return texture.sample(sampler, coord, ANGLE_gradient<N>(dPdx, dPdy), offset);
}
)",
                        gradient(),
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_generic_float3_float2_float2_int2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy), offset);
}
)",
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_generic_float4_float2_float2_int2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy), offset);
}
)",
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_depth2d_float3_float2_float2_int2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    metal::int2 const offset)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, coord.z, metal::gradient2d(dPdx, dPdy), offset));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, metal::gradient2d(dPdx, dPdy), offset) > coord.z);
    }
}
)",
                        functionConstants(),
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_depth2darray_float4_float2_float2_int2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread metal::depth2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    metal::int2 const offset)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, uint(metal::round(coord.z)), coord.w, metal::gradient2d(dPdx, dPdy), offset));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::gradient2d(dPdx, dPdy), offset) > coord.w);
    }
}
)",
                        functionConstants(),
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_depthcube_float4_float3_float3_int3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread metal::depthcube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy,
    metal::int3 const offset)
{
    return texture.sample_compare(sampler, coord.xyz, coord.w, metal::gradientcube(dPdx, dPdy), offset);
}
)",
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureGradOffset_texturecube_float3_float3_float3_int3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureGradOffset_impl(
    thread metal::texturecube<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy,
    metal::int3 const offset)
{
    return texture.sample(sampler, coord, metal::gradientcube(dPdx, dPdy), offset);
}
)",
                        textureGradOffset())

PROGRAM_PRELUDE_DECLARE(textureLod,
                        R"(
#define ANGLE_textureLod(env, ...) ANGLE_textureLod_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureLod_generic_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureLod())

PROGRAM_PRELUDE_DECLARE(textureLod_generic_float3,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord, metal::level(level));
}
)",
                        textureLod())

PROGRAM_PRELUDE_DECLARE(textureLod_depth2d_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLod_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    if (ANGLE_UseSampleCompareLod)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, coord.z, metal::level(level)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, metal::level(level)) > coord.z);
    }
}
)",
                        functionConstants(),
                        textureLod())

PROGRAM_PRELUDE_DECLARE(textureLod_texture2darray_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLod_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::level(level));
}
)",
                        textureLod())

PROGRAM_PRELUDE_DECLARE(textureLod_texture2darray_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLod_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xyz, uint(metal::round(coord.w)), metal::level(level));
}
)",
                        textureLod())

PROGRAM_PRELUDE_DECLARE(textureLodOffset,
                        R"(
#define ANGLE_textureLodOffset(env, ...) ANGLE_textureLodOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLodOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    float level,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord, metal::level(level), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLodOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level,
    metal::int3 const offset)
{
    return texture.sample(sampler, coord, metal::level(level), offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLodOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level,
    int2 const offset)
{
    if (ANGLE_UseSampleCompareLod)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy, coord.z, metal::level(level), offset));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy, metal::level(level), offset) > coord.z);
    }
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLodOffset_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::level(level), offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureLodOffset_impl(
    thread metal::texture2d_array<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level,
    metal::int3 const offset)
{
    return texture.sample(sampler, coord.xyz, uint(metal::round(coord.w)), metal::level(level), offset);
}
)",
                        functionConstants(),
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureOffset,
                        R"(
#define ANGLE_textureOffset(env, ...) ANGLE_textureOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord, offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float2 const coord,
    metal::int2 const offset,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int2 const offset)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int2 const offset,
    float bias)
{
    return texture.sample(sampler, coord.xy, uint(metal::round(coord.z)), metal::bias(bias), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int3 const offset)
{
    return texture.sample(sampler, coord, offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int3 const offset,
    float bias)
{
    return texture.sample(sampler, coord, metal::bias(bias), offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int2 const offset)
{
    return texture.sample_compare(sampler, coord.xy, coord.z, offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::int2 const offset,
    float bias)
{
    return texture.sample_compare(sampler, coord.xy, coord.z, metal::bias(bias), offset);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProj,
                        R"(
#define ANGLE_textureProj(env, ...) ANGLE_textureProj_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::bias(bias));
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProj_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::bias(bias));
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProj_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float bias = 0)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::bias(bias));
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProjGrad,
                        R"(
#define ANGLE_textureProjGrad(env, ...) ANGLE_textureProjGrad_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProjGrad_generic_float3_float2_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGrad_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::gradient2d(dPdx, dPdy));
}
)",
                        textureProjGrad())

PROGRAM_PRELUDE_DECLARE(textureProjGrad_generic_float4_float2_float2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGrad_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::gradient2d(dPdx, dPdy));
}
)",
                        textureProjGrad())

PROGRAM_PRELUDE_DECLARE(textureProjGrad_depth2d_float4_float2_float2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGrad_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy/coord.w, coord.z/coord.w, metal::gradient2d(dPdx, dPdy)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy/coord.w, metal::gradient2d(dPdx, dPdy)) > coord.z/coord.w);
    }
}
)",
                        functionConstants(),
                        textureProjGrad())

PROGRAM_PRELUDE_DECLARE(textureProjGrad_texture3d_float4_float3_float3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGrad_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::gradient3d(dPdx, dPdy));
}
)",
                        textureProjGrad())

PROGRAM_PRELUDE_DECLARE(textureProjGradOffset,
                        R"(
#define ANGLE_textureProjGradOffset(env, ...) ANGLE_textureProjGradOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProjGradOffset_generic_float3_float2_float2_int2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGradOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    int2 const offset)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::gradient2d(dPdx, dPdy), offset);
}
)",
                        textureProjGradOffset())

PROGRAM_PRELUDE_DECLARE(textureProjGradOffset_generic_float4_float2_float2_int2,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGradOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    int2 const offset)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::gradient2d(dPdx, dPdy), offset);
}
)",
                        textureProjGradOffset())

PROGRAM_PRELUDE_DECLARE(textureProjGradOffset_depth2d_float4_float2_float2_int2,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGradOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float2 const dPdx,
    metal::float2 const dPdy,
    int2 const offset)
{
    if (ANGLE_UseSampleCompareGradient)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy/coord.w, coord.z/coord.w, metal::gradient2d(dPdx, dPdy), offset));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy/coord.w, metal::gradient2d(dPdx, dPdy), offset) > coord.z/coord.w);
    }
}
)",
                        functionConstants(),
                        textureProjGradOffset())

PROGRAM_PRELUDE_DECLARE(textureProjGradOffset_texture3d_float4_float3_float3_int3,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjGradOffset_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    metal::float3 const dPdx,
    metal::float3 const dPdy,
    int3 const offset)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::gradient3d(dPdx, dPdy), offset);
}
)",
                        textureProjGradOffset())

PROGRAM_PRELUDE_DECLARE(textureProjLod,
                        R"(
#define ANGLE_textureProjLod(env, ...) ANGLE_textureProjLod_impl(*env.texture, *env.sampler, __VA_ARGS__)
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProjLod_generic_float3,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::level(level));
}
)",
                        textureProjLod())

PROGRAM_PRELUDE_DECLARE(textureProjLod_generic_float4,
                        R"(
template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLod_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::level(level));
}
)",
                        textureProjLod())

PROGRAM_PRELUDE_DECLARE(textureProjLod_depth2d_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLod_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    if (ANGLE_UseSampleCompareLod)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy/coord.w, coord.z/coord.w, metal::level(level)));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy/coord.w, metal::level(level)) > coord.z/coord.w);
    }
}
)",
                        functionConstants(),
                        textureProjLod())

PROGRAM_PRELUDE_DECLARE(textureProjLod_texture3d_float4,
                        R"(
template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLod_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::level(level));
}
)",
                        textureProjLod())

PROGRAM_PRELUDE_DECLARE(textureProjLodOffset,
                        R"(
#define ANGLE_textureProjLodOffset(env, ...) ANGLE_textureProjLodOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLodOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    float level,
    int2 const offset)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::level(level), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLodOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level,
    int2 const offset)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::level(level), offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLodOffset_impl(
    thread metal::depth2d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level,
    int2 const offset)
{
    if (ANGLE_UseSampleCompareLod)
    {
        return static_cast<T>(texture.sample_compare(sampler, coord.xy/coord.w, coord.z/coord.w, metal::level(level), offset));
    }
    else
    {
        return static_cast<T>(texture.sample(sampler, coord.xy/coord.w, metal::level(level), offset) > coord.z/coord.w);
    }
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjLodOffset_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    float level,
    int3 const offset)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::level(level), offset);
}
)",
                        functionConstants(),
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureProjOffset,
                        R"(
#define ANGLE_textureProjOffset(env, ...) ANGLE_textureProjOffset_impl(*env.texture, *env.sampler, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float3 const coord,
    int2 const offset,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.z, metal::bias(bias), offset);
}

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjOffset_impl(
    thread Texture &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    int2 const offset,
    float bias = 0)
{
    return texture.sample(sampler, coord.xy/coord.w, metal::bias(bias), offset);
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureProjOffset_impl(
    thread metal::texture3d<T> &texture,
    thread metal::sampler const &sampler,
    metal::float4 const coord,
    int3 const offset,
    float bias = 0)
{
    return texture.sample(sampler, coord.xyz/coord.w, metal::bias(bias), offset);
}
)",
                        textureEnv())

PROGRAM_PRELUDE_DECLARE(textureSize,
                        R"(
#define ANGLE_textureSize(env, ...) ANGLE_textureSize_impl(*env.texture, __VA_ARGS__)

template <typename Texture>
ANGLE_ALWAYS_INLINE auto ANGLE_textureSize_impl(
    thread Texture &texture,
    int level)
{
    return int2(texture.get_width(uint(level)), texture.get_height(uint(level)));
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureSize_impl(
    thread metal::texture3d<T> &texture,
    int level)
{
    return int3(texture.get_width(uint(level)), texture.get_height(uint(level)), texture.get_depth(uint(level)));
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureSize_impl(
    thread metal::depth2d_array<T> &texture,
    int level)
{
    return int3(texture.get_width(uint(level)), texture.get_height(uint(level)), texture.get_array_size());
}

template <typename T>
ANGLE_ALWAYS_INLINE auto ANGLE_textureSize_impl(
    thread metal::texture2d_array<T> &texture,
    int level)
{
    return int3(texture.get_width(uint(level)), texture.get_height(uint(level)), texture.get_array_size());
}
)",
                        textureEnv())

////////////////////////////////////////////////////////////////////////////////

// Returned Name is valid for as long as `buffer` is still alive.
// Returns false if no template args exist.
// Returns false if buffer is not large enough.
//
// Example:
//  "foo<1,2>" --> "foo<>"
static std::pair<Name, bool> MaskTemplateArgs(const Name &name, size_t bufferSize, char *buffer)
{
    const char *begin = name.rawName().data();
    const char *end   = strchr(begin, '<');
    if (!end)
    {
        return {{}, false};
    }
    size_t n = end - begin;
    if (n + 3 > bufferSize)
    {
        return {{}, false};
    }
    for (size_t i = 0; i < n; ++i)
    {
        buffer[i] = begin[i];
    }
    buffer[n + 0] = '<';
    buffer[n + 1] = '>';
    buffer[n + 2] = '\0';
    return {Name(buffer, name.symbolType()), true};
}

ProgramPrelude::FuncToEmitter ProgramPrelude::BuildFuncToEmitter()
{
#define EMIT_METHOD(method) \
    [](ProgramPrelude &pp, const TFunction &) -> void { return pp.method(); }
    FuncToEmitter map;

    auto put = [&](Name name, FuncEmitter emitter) {
        FuncEmitter &dest = map[name];
        ASSERT(!dest);
        dest = emitter;
    };

    auto putAngle = [&](const char *nameStr, FuncEmitter emitter) {
        Name name(nameStr, SymbolType::AngleInternal);
        put(name, emitter);
    };

    auto putBuiltIn = [&](const char *nameStr, FuncEmitter emitter) {
        Name name(nameStr, SymbolType::BuiltIn);
        put(name, emitter);
    };

    putAngle("addressof", EMIT_METHOD(addressof));
    putAngle("cast<>", EMIT_METHOD(castMatrix));
    putAngle("elem_ref", EMIT_METHOD(vectorElemRef));
    putAngle("flatten", EMIT_METHOD(flattenArray));
    putAngle("inout", EMIT_METHOD(inout));
    putAngle("out", EMIT_METHOD(out));
    putAngle("swizzle_ref", EMIT_METHOD(swizzleRef));

    putBuiltIn("texelFetch", EMIT_METHOD(texelFetch));
    putBuiltIn("texelFetchOffset", EMIT_METHOD(texelFetchOffset));
    putBuiltIn("texture", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        const bool bias             = func.getParamCount() >= 3;
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3)
            {
                if (bias)
                {
                    return pp.texture_depth2d_float3_float();
                }
                return pp.texture_depth2d_float3();
            }
        }
        if (textureName.beginsWith("metal::depthcube<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                if (bias)
                {
                    return pp.texture_depthcube_float4_float();
                }
                return pp.texture_depthcube_float4();
            }
        }
        if (textureName.beginsWith("metal::depth2d_array<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                if (bias)
                {
                    return pp.texture_depth2darray_float4_float();
                }
                return pp.texture_depth2darray_float4();
            }
        }
        if (textureName.beginsWith("metal::texture2d_array<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3)
            {
                if (bias)
                {
                    return pp.texture_texture2darray_float3_float();
                }
                return pp.texture_texture2darray_float3();
            }
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                if (bias)
                {
                    return pp.texture_texture2darray_float4_float();
                }
                return pp.texture_texture2darray_float4();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 2)
        {
            if (bias)
            {
                return pp.texture_generic_float2_float();
            }
            return pp.texture_generic_float2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3)
        {
            if (bias)
            {
                return pp.texture_generic_float3_float();
            }
            return pp.texture_generic_float3();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("texture1DLod", EMIT_METHOD(texture1DLod));
    putBuiltIn("texture1DProj", EMIT_METHOD(texture1DProj));
    putBuiltIn("texture1DProjLod", EMIT_METHOD(texture1DProjLod));
    putBuiltIn("texture2D", EMIT_METHOD(texture2D));
    putBuiltIn("texture2DLod", EMIT_METHOD(texture2DLod));
    putBuiltIn("texture2DProj", EMIT_METHOD(texture2DProj));
    putBuiltIn("texture2DRect", EMIT_METHOD(texture2DRect));
    putBuiltIn("texture2DRectProj", EMIT_METHOD(texture2DRectProj));
    putBuiltIn("texture3DLod", EMIT_METHOD(texture3DLod));
    putBuiltIn("texture3DProj", EMIT_METHOD(texture3DProj));
    putBuiltIn("texture3DProjLod", EMIT_METHOD(texture3DProjLod));
    putBuiltIn("textureCube", EMIT_METHOD(textureCube));
    putBuiltIn("textureCubeLod", EMIT_METHOD(textureCubeLod));
    putBuiltIn("textureCubeProj", EMIT_METHOD(textureCubeProj));
    putBuiltIn("textureCubeProjLod", EMIT_METHOD(textureCubeProjLod));
    putBuiltIn("texture2DProjLod", EMIT_METHOD(texture2DProjLod));
    putBuiltIn("textureGrad", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        const TType &dPdx           = func.getParam(2)->getType();
        const int dPdxN             = dPdx.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
            {
                return pp.textureGrad_depth2d_float3_float2_float2();
            }
        }
        if (textureName.beginsWith("metal::depth2d_array<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
            {
                return pp.textureGrad_depth2darray_float4_float2_float2();
            }
        }
        if (textureName.beginsWith("metal::depthcube<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 3)
            {
                return pp.textureGrad_depthcube_float4_float3_float3();
            }
        }
        if (textureName.beginsWith("metal::texturecube<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 3)
            {
                return pp.textureGrad_texturecube_float3_float3_float3();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
        {
            return pp.textureGrad_generic_float3_float2_float2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
        {
            return pp.textureGrad_generic_float4_float2_float2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == dPdxN)
        {
            return pp.textureGrad_generic_floatN_floatN_floatN();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureGradOffset", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        const TType &dPdx           = func.getParam(2)->getType();
        const int dPdxN             = dPdx.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
            {
                return pp.textureGradOffset_depth2d_float3_float2_float2_int2();
            }
        }
        if (textureName.beginsWith("metal::depth2d_array<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
            {
                return pp.textureGradOffset_depth2darray_float4_float2_float2_int2();
            }
        }
        if (textureName.beginsWith("metal::depthcube<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 3)
            {
                return pp.textureGradOffset_depthcube_float4_float3_float3_int3();
            }
        }
        if (textureName.beginsWith("metal::texturecube<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 3)
            {
                return pp.textureGradOffset_texturecube_float3_float3_float3_int3();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
        {
            return pp.textureGradOffset_generic_float3_float2_float2_int2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
        {
            return pp.textureGradOffset_generic_float4_float2_float2_int2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == dPdxN)
        {
            return pp.textureGradOffset_generic_floatN_floatN_floatN_intN();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureLod", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3)
            {
                return pp.textureLod_depth2d_float3();
            }
        }
        if (textureName.beginsWith("metal::texture2d_array<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 3)
            {
                return pp.textureLod_texture2darray_float3();
            }
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                return pp.textureLod_texture2darray_float4();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 2)
        {
            return pp.textureLod_generic_float2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3)
        {
            return pp.textureLod_generic_float3();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureLodOffset", EMIT_METHOD(textureLodOffset));
    putBuiltIn("textureOffset", EMIT_METHOD(textureOffset));
    putBuiltIn("textureProj", EMIT_METHOD(textureProj));
    putBuiltIn("textureProjGrad", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        const TType &dPdx           = func.getParam(2)->getType();
        const int dPdxN             = dPdx.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
            {
                return pp.textureProjGrad_depth2d_float4_float2_float2();
            }
        }
        if (textureName.beginsWith("metal::texture3d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 3)
            {
                return pp.textureProjGrad_texture3d_float4_float3_float3();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
        {
            return pp.textureProjGrad_generic_float3_float2_float2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
        {
            return pp.textureProjGrad_generic_float4_float2_float2();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureProjGradOffset", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        const TType &dPdx           = func.getParam(2)->getType();
        const int dPdxN             = dPdx.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
            {
                return pp.textureProjGradOffset_depth2d_float4_float2_float2_int2();
            }
        }
        if (textureName.beginsWith("metal::texture3d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 3)
            {
                return pp.textureProjGradOffset_texture3d_float4_float3_float3_int3();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3 && dPdxN == 2)
        {
            return pp.textureProjGradOffset_generic_float3_float2_float2_int2();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 4 && dPdxN == 2)
        {
            return pp.textureProjGradOffset_generic_float4_float2_float2_int2();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureProjLod", [](ProgramPrelude &pp, const TFunction &func) {
        const ImmutableString textureName =
            GetTextureTypeName(func.getParam(0)->getType().getBasicType()).rawName();
        const TType &coord          = func.getParam(1)->getType();
        const TBasicType coordBasic = coord.getBasicType();
        const int coordN            = coord.getNominalSize();
        if (textureName.beginsWith("metal::depth2d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                return pp.textureProjLod_depth2d_float4();
            }
        }
        if (textureName.beginsWith("metal::texture3d<"))
        {
            if (coordBasic == TBasicType::EbtFloat && coordN == 4)
            {
                return pp.textureProjLod_texture3d_float4();
            }
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 3)
        {
            return pp.textureProjLod_generic_float3();
        }
        if (coordBasic == TBasicType::EbtFloat && coordN == 4)
        {
            return pp.textureProjLod_generic_float4();
        }
        UNIMPLEMENTED();
    });
    putBuiltIn("textureProjLodOffset", EMIT_METHOD(textureProjLodOffset));
    putBuiltIn("textureProjOffset", EMIT_METHOD(textureProjOffset));
    putBuiltIn("textureSize", EMIT_METHOD(textureSize));

    return map;

#undef EMIT_METHOD
}

void ProgramPrelude::visitOperator(TOperator op,
                                   const TFunction *func,
                                   const TType *argType0,
                                   const TType *argType1)
{
    switch (op)
    {
        case TOperator::EOpRadians:
            radians();
            break;
        case TOperator::EOpDegrees:
            degrees();
            break;
        case TOperator::EOpAtan:
            atan();
            break;
        case TOperator::EOpMod:
            mod();
            break;
        case TOperator::EOpRefract:
            refract();
            break;
        case TOperator::EOpDistance:
            distance();
            break;
        case TOperator::EOpLength:
            length();
            break;
        case TOperator::EOpDot:
            dot();
            break;
        case TOperator::EOpNormalize:
            normalize();
            break;
        case TOperator::EOpFaceforward:
            faceforward();
            break;
        case TOperator::EOpReflect:
            reflect();
            break;

        case TOperator::EOpSin:
        case TOperator::EOpCos:
        case TOperator::EOpTan:
        case TOperator::EOpAsin:
        case TOperator::EOpAcos:
        case TOperator::EOpSinh:
        case TOperator::EOpCosh:
        case TOperator::EOpTanh:
        case TOperator::EOpAsinh:
        case TOperator::EOpAcosh:
        case TOperator::EOpAtanh:
        case TOperator::EOpAbs:
        case TOperator::EOpFma:
        case TOperator::EOpPow:
        case TOperator::EOpExp:
        case TOperator::EOpExp2:
        case TOperator::EOpLog:
        case TOperator::EOpLog2:
        case TOperator::EOpSqrt:
        case TOperator::EOpFloor:
        case TOperator::EOpTrunc:
        case TOperator::EOpCeil:
        case TOperator::EOpFract:
        case TOperator::EOpRound:
        case TOperator::EOpRoundEven:
        case TOperator::EOpModf:
        case TOperator::EOpLdexp:
        case TOperator::EOpFrexp:
        case TOperator::EOpInversesqrt:
            include_metal_math();
            break;

        case TOperator::EOpEqual:
            if (argType0->isVector() && argType1->isVector())
            {
                equalVector();
            }
            // Even if Arg0 is a vector or matrix, it could also be an array.
            if (argType0->isArray() && argType1->isArray())
            {
                equalArray();
            }
            if (argType0->getStruct() && argType1->getStruct() && argType0->isArray() &&
                argType1->isArray())
            {
                equalStructArray();
            }
            if (argType0->isMatrix() && argType1->isMatrix())
            {
                equalMatrix();
            }
            break;

        case TOperator::EOpNotEqual:
            if (argType0->isVector() && argType1->isVector())
            {
                notEqualVector();
            }
            else if (argType0->getStruct() && argType1->getStruct())
            {
                notEqualStruct();
            }
            // Same as above.
            if (argType0->isArray() && argType1->isArray())
            {
                notEqualArray();
            }
            if (argType0->getStruct() && argType1->getStruct() && argType0->isArray() &&
                argType1->isArray())
            {
                notEqualStructArray();
            }
            if (argType0->isMatrix() && argType1->isMatrix())
            {
                notEqualMatrix();
            }
            break;

        case TOperator::EOpCross:
            include_metal_geometric();
            break;

        case TOperator::EOpSign:
            sign();
            break;

        case TOperator::EOpClamp:
        case TOperator::EOpMin:
        case TOperator::EOpMax:
        case TOperator::EOpMix:
        case TOperator::EOpStep:
        case TOperator::EOpSmoothstep:
            include_metal_common();
            break;

        case TOperator::EOpAll:
        case TOperator::EOpAny:
        case TOperator::EOpIsnan:
        case TOperator::EOpIsinf:
            include_metal_relational();
            break;

        case TOperator::EOpDFdx:
        case TOperator::EOpDFdy:
        case TOperator::EOpFwidth:
            include_metal_graphics();
            break;

        case TOperator::EOpTranspose:
        case TOperator::EOpDeterminant:
            include_metal_matrix();
            break;

        case TOperator::EOpAdd:
            if (argType0->isMatrix() && argType1->isScalar())
            {
                addMatrixScalar();
            }
            break;

        case TOperator::EOpAddAssign:
            if (argType0->isMatrix() && argType1->isScalar())
            {
                addMatrixScalarAssign();
            }
            break;

        case TOperator::EOpSub:
            if (argType0->isMatrix() && argType1->isScalar())
            {
                subMatrixScalar();
            }
            break;

        case TOperator::EOpSubAssign:
            if (argType0->isMatrix() && argType1->isScalar())
            {
                subMatrixScalarAssign();
            }
            break;

        case TOperator::EOpDiv:
            if (argType0->isMatrix())
            {
                if (argType1->isMatrix())
                {
                    componentWiseDivide();
                }
                else if (argType1->isScalar())
                {
                    divMatrixScalar();
                }
            }
            break;

        case TOperator::EOpDivAssign:
            if (argType0->isMatrix() && argType1->isMatrix())
            {
                componentWiseDivideAssign();
            }
            break;

        case TOperator::EOpMulMatrixComponentWise:
            if (argType0->isMatrix() && argType1->isMatrix())
            {
                componentWiseMultiply();
            }
            break;

        case TOperator::EOpOuterProduct:
            outerProduct();
            break;

        case TOperator::EOpInverse:
            switch (argType0->getCols())
            {
                case 2:
                    inverse2();
                    break;
                case 3:
                    inverse3();
                    break;
                case 4:
                    inverse4();
                    break;
                default:
                    UNREACHABLE();
            }
            break;

        case TOperator::EOpMatrixTimesMatrixAssign:
            matmulAssign();
            break;

        case TOperator::EOpPreIncrement:
            if (argType0->isMatrix())
            {
                preIncrementMatrix();
            }
            break;

        case TOperator::EOpPostIncrement:
            if (argType0->isMatrix())
            {
                postIncrementMatrix();
            }
            break;

        case TOperator::EOpPreDecrement:
            if (argType0->isMatrix())
            {
                preDecrementMatrix();
            }
            break;

        case TOperator::EOpPostDecrement:
            if (argType0->isMatrix())
            {
                postDecrementMatrix();
            }
            break;

            break;

        case TOperator::EOpNegative:
            if (argType0->isMatrix())
            {
                negateMatrix();
            }
            break;

        case TOperator::EOpComma:
        case TOperator::EOpAssign:
        case TOperator::EOpInitialize:
        case TOperator::EOpMulAssign:
        case TOperator::EOpIModAssign:
        case TOperator::EOpBitShiftLeftAssign:
        case TOperator::EOpBitShiftRightAssign:
        case TOperator::EOpBitwiseAndAssign:
        case TOperator::EOpBitwiseXorAssign:
        case TOperator::EOpBitwiseOrAssign:
        case TOperator::EOpMul:
        case TOperator::EOpIMod:
        case TOperator::EOpBitShiftLeft:
        case TOperator::EOpBitShiftRight:
        case TOperator::EOpBitwiseAnd:
        case TOperator::EOpBitwiseXor:
        case TOperator::EOpBitwiseOr:
        case TOperator::EOpLessThan:
        case TOperator::EOpGreaterThan:
        case TOperator::EOpLessThanEqual:
        case TOperator::EOpGreaterThanEqual:
        case TOperator::EOpLessThanComponentWise:
        case TOperator::EOpLessThanEqualComponentWise:
        case TOperator::EOpGreaterThanEqualComponentWise:
        case TOperator::EOpGreaterThanComponentWise:
        case TOperator::EOpLogicalOr:
        case TOperator::EOpLogicalXor:
        case TOperator::EOpLogicalAnd:
        case TOperator::EOpPositive:
        case TOperator::EOpLogicalNot:
        case TOperator::EOpLogicalNotComponentWise:
        case TOperator::EOpBitwiseNot:
        case TOperator::EOpVectorTimesScalarAssign:
        case TOperator::EOpVectorTimesMatrixAssign:
        case TOperator::EOpMatrixTimesScalarAssign:
        case TOperator::EOpVectorTimesScalar:
        case TOperator::EOpVectorTimesMatrix:
        case TOperator::EOpMatrixTimesVector:
        case TOperator::EOpMatrixTimesScalar:
        case TOperator::EOpMatrixTimesMatrix:
        case TOperator::EOpReturn:
        case TOperator::EOpBreak:
        case TOperator::EOpContinue:
        case TOperator::EOpEqualComponentWise:
        case TOperator::EOpNotEqualComponentWise:
        case TOperator::EOpIndexDirect:
        case TOperator::EOpIndexIndirect:
        case TOperator::EOpIndexDirectStruct:
        case TOperator::EOpIndexDirectInterfaceBlock:
        case TOperator::EOpFloatBitsToInt:
        case TOperator::EOpIntBitsToFloat:
        case TOperator::EOpUintBitsToFloat:
        case TOperator::EOpFloatBitsToUint:
        case TOperator::EOpNull:
            // do nothing
            break;

        case TOperator::EOpKill:
            include_metal_graphics();
            break;

        case TOperator::EOpPackUnorm2x16:
        case TOperator::EOpPackSnorm2x16:
        case TOperator::EOpPackUnorm4x8:
        case TOperator::EOpPackSnorm4x8:
        case TOperator::EOpUnpackSnorm2x16:
        case TOperator::EOpUnpackUnorm2x16:
        case TOperator::EOpUnpackUnorm4x8:
        case TOperator::EOpUnpackSnorm4x8:
            include_metal_pack();
            break;

        case TOperator::EOpPackHalf2x16:
            pack_half_2x16();
            break;
        case TOperator::EOpUnpackHalf2x16:
            unpack_half_2x16();
            break;

        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::EOpBarrier:
        case TOperator::EOpMemoryBarrier:
        case TOperator::EOpMemoryBarrierAtomicCounter:
        case TOperator::EOpMemoryBarrierBuffer:
        case TOperator::EOpMemoryBarrierImage:
        case TOperator::EOpMemoryBarrierShared:
        case TOperator::EOpGroupMemoryBarrier:
        case TOperator::EOpAtomicAdd:
        case TOperator::EOpAtomicMin:
        case TOperator::EOpAtomicMax:
        case TOperator::EOpAtomicAnd:
        case TOperator::EOpAtomicOr:
        case TOperator::EOpAtomicXor:
        case TOperator::EOpAtomicExchange:
        case TOperator::EOpAtomicCompSwap:
        case TOperator::EOpEmitVertex:
        case TOperator::EOpEndPrimitive:
        case TOperator::EOpFTransform:
        case TOperator::EOpPackDouble2x32:
        case TOperator::EOpUnpackDouble2x32:
        case TOperator::EOpArrayLength:
            UNIMPLEMENTED();
            break;

        case TOperator::EOpConstruct:
            ASSERT(!func);
            break;

        case TOperator::EOpCallFunctionInAST:
        case TOperator::EOpCallInternalRawFunction:
        case TOperator::EOpCallBuiltInFunction:
            ASSERT(func);
            if (mHandled.insert(func).second)
            {
                const Name name(*func);
                const auto end = mFuncToEmitter.end();
                auto iter      = mFuncToEmitter.find(name);
                if (iter == end)
                {
                    char buffer[32];
                    auto mask = MaskTemplateArgs(name, sizeof(buffer), buffer);
                    if (mask.second)
                    {
                        iter = mFuncToEmitter.find(mask.first);
                    }
                }
                if (iter != end)
                {
                    const auto &emitter = iter->second;
                    emitter(*this, *func);
                }
            }
            break;
    }
}

void ProgramPrelude::visitVariable(const Name &name, const TType &type)
{
    if (const TStructure *s = type.getStruct())
    {
        const Name typeName(*s);
        if (typeName.beginsWith(Name("TextureEnv<")))
        {
            textureEnv();
        }
    }
    else
    {
        if (name == constant_names::kRasterizationDiscardEnabled)
        {
            functionConstants();
        }
    }
}

void ProgramPrelude::visitVariable(const TVariable &var)
{
    if (mHandled.insert(&var).second)
    {
        visitVariable(Name(var), var.getType());
    }
}

void ProgramPrelude::visitStructure(const TStructure &s)
{
    if (mHandled.insert(&s).second)
    {
        for (const TField *field : s.fields())
        {
            const TType &type = *field->type();
            visitVariable(Name(*field), type);
        }
    }
}

bool ProgramPrelude::visitBinary(Visit visit, TIntermBinary *node)
{
    const TType &leftType  = node->getLeft()->getType();
    const TType &rightType = node->getRight()->getType();
    visitOperator(node->getOp(), nullptr, &leftType, &rightType);
    return true;
}

bool ProgramPrelude::visitUnary(Visit visit, TIntermUnary *node)
{
    const TType &argType = node->getOperand()->getType();
    visitOperator(node->getOp(), nullptr, &argType);
    return true;
}

bool ProgramPrelude::visitAggregate(Visit visit, TIntermAggregate *node)
{
    const size_t argCount = node->getChildCount();

    auto getArgType = [node, argCount](size_t index) -> const TType & {
        ASSERT(index < argCount);
        TIntermTyped *arg = node->getChildNode(index)->getAsTyped();
        ASSERT(arg);
        return arg->getType();
    };

    const TFunction *func = node->getFunction();

    switch (node->getChildCount())
    {
        case 0:
        {
            visitOperator(node->getOp(), func, nullptr);
        }
        break;

        case 1:
        {
            const TType &argType0 = getArgType(0);
            visitOperator(node->getOp(), func, &argType0);
        }
        break;

        case 2:
        {
            const TType &argType0 = getArgType(0);
            const TType &argType1 = getArgType(1);
            visitOperator(node->getOp(), func, &argType0, &argType1);
        }
        break;

        default:
        {
            const TType &argType0 = getArgType(0);
            const TType &argType1 = getArgType(1);
            visitOperator(node->getOp(), func, &argType0, &argType1);
        }
        break;
    }

    return true;
}

bool ProgramPrelude::visitDeclaration(Visit, TIntermDeclaration *node)
{
    Declaration decl  = ViewDeclaration(*node);
    const TType &type = decl.symbol.getType();
    if (type.isStructSpecifier())
    {
        const TStructure *s = type.getStruct();
        ASSERT(s);
        visitStructure(*s);
    }
    return true;
}

void ProgramPrelude::visitSymbol(TIntermSymbol *node)
{
    visitVariable(node->variable());
}

bool sh::EmitProgramPrelude(TIntermBlock &root, TInfoSinkBase &out, const ProgramPreludeConfig &ppc)
{
    ProgramPrelude programPrelude(out, ppc);
    root.traverse(&programPrelude);
    return true;
}
