// SPDX-License-Identifier: BSD-3-Clause
// Copyright Contributors to the OpenColorIO Project.


#include <cmath>

#include <OpenColorIO/OpenColorIO.h>

#include <Imath/half.h>

#include "ops/fixedfunction/FixedFunctionOp.h"
#include "ops/gradingrgbcurve/GradingRGBCurveOp.h"
#include "ops/log/LogOp.h"
#include "ops/matrix/MatrixOp.h"
#include "ops/range/RangeOp.h"
#include "transforms/builtins/ACES.h"
#include "transforms/builtins/BuiltinTransformRegistry.h"
#include "transforms/builtins/OpHelpers.h"


namespace OCIO_NAMESPACE
{

//
// Define component functions for reuse in multiple built-ins.
//

namespace AP1_to_CIE_XYZ_D65
{

void GenerateOps(OpRcPtrVec & ops)
{
    MatrixOpData::MatrixArrayPtr matrix
        = build_conversion_matrix_to_XYZ_D65(ACES_AP1::primaries, ADAPTATION_BRADFORD);
    CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
}

}  // namespace AP1_to_CIE_XYZ_D65

namespace ACEScct_to_LINEAR
{

static constexpr double linSideSlope  = 1.;
static constexpr double linSideOffset = 0.;
static constexpr double logSideSlope  = 1. / 17.52;
static constexpr double logSideOffset = 9.72 / 17.52;
static constexpr double linSideBreak  = 0.0078125;
static constexpr double base          = 2.;

static const LogOpData::Params 
    params { logSideSlope, logSideOffset, linSideSlope, linSideOffset, linSideBreak };

static const LogOpData log(base, params, params, params, TRANSFORM_DIR_INVERSE);

}  // namespace ACEScct_to_LINEAR

namespace ADX_to_ACES
{

static constexpr unsigned lutSize = 11;
static constexpr double nonuniform_LUT[lutSize * 2]
{
    -0.190000000000000, -6.000000000000000,
     0.010000000000000, -2.721718645000000,
     0.028000000000000, -2.521718645000000,
     0.054000000000000, -2.321718645000000,
     0.095000000000000, -2.121718645000000,
     0.145000000000000, -1.921718645000000,
     0.220000000000000, -1.721718645000000,
     0.300000000000000, -1.521718645000000,
     0.400000000000000, -1.321718645000000,
     0.500000000000000, -1.121718645000000,
     0.600000000000000, -0.926545676714876
};

void GenerateOps(OpRcPtrVec & ops)
{
    // Note that in CTL, the matrices are stored transposed.
    static constexpr double CDD_TO_CID[4 * 4]
    {
        0.75573,  0.22197,  0.02230,  0.,
        0.05901,  0.96928, -0.02829,  0.,
        0.16134,  0.07406,  0.76460,  0.,
        0.,       0.,       0.,       1.
    };

    // Convert Channel Dependent Density values into Channel Independent Density values.
    CreateMatrixOp(ops, &CDD_TO_CID[0], TRANSFORM_DIR_FORWARD);

    auto GenerateLutValues = [](double in) -> float
    {
        double out = 0.;

        if (in < nonuniform_LUT[0]) // Lower bound i.e. in < nonuniform_LUT[0, 0].
        {
            // Extrapolate to ease conversion to LUT1D.
            const double slope = (nonuniform_LUT[3] - nonuniform_LUT[1])
                                    / (nonuniform_LUT[2] - nonuniform_LUT[0]);

            out = nonuniform_LUT[1] - slope * (nonuniform_LUT[0] - in);

            if (out < -10.) out = -10.;
        }
        else if (in <= nonuniform_LUT[(lutSize-1) * 2])
        {
            out = Interpolate1D(lutSize, &nonuniform_LUT[0], in);
        }
        else // Upper bound i.e. in > nonuniform_LUT[lutSize-1, 0].
        {
            const double REF_PT
                = (7120. - 1520.) / 8000. * (100. / 55.) - std::log10(0.18);

            out = (100. / 55.) * in - REF_PT;

            if (out > 4.8162678) out = 4.8162678;  // log10(HALF_MAX)
        }

        return float(out);
    };

    // Convert Channel Independent Density values to Relative Log Exposure values.
    CreateHalfLut(ops, GenerateLutValues);

    // Convert Relative Log Exposure values to Relative Exposure values.
    CreateLogOp(ops, 10., TRANSFORM_DIR_INVERSE);

    static constexpr double EXP_TO_ACES[4 * 4]
    {
        0.72286,  0.12630,  0.15084,  0.,
        0.11923,  0.76418,  0.11659,  0.,
        0.01427,  0.08213,  0.90359,  0.,
        0.,       0.,       0.,       1.
    };

    // Convert Relative Exposure values to ACES values.
    CreateMatrixOp(ops, &EXP_TO_ACES[0], TRANSFORM_DIR_FORWARD);            
}

}  // namespace ADX_to_ACES

namespace ACES_OUTPUT
{

void Generate_RRT_preamble_ops(OpRcPtrVec & ops)
{
    CreateFixedFunctionOp(ops, FixedFunctionOpData::ACES_GLOW_10_FWD, {});

    CreateFixedFunctionOp(ops, FixedFunctionOpData::ACES_RED_MOD_10_FWD, {});

    CreateRangeOp(ops, 
                  0., RangeOpData::EmptyValue(),  // don't clamp high end
                  0., RangeOpData::EmptyValue(),  // don't clamp high end
                  TRANSFORM_DIR_FORWARD);

    MatrixOpData::MatrixArrayPtr matrix
        = build_conversion_matrix(ACES_AP0::primaries, ACES_AP1::primaries, ADAPTATION_NONE);
    CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);

    CreateRangeOp(ops, 
                  0., RangeOpData::EmptyValue(),  // don't clamp high end
                  0., RangeOpData::EmptyValue(),  // don't clamp high end
                  TRANSFORM_DIR_FORWARD);

    static constexpr double RRT_SAT_MAT[4 * 4]
    {
        0.970889148671, 0.026963270632, 0.002147580696, 0., 
        0.010889148671, 0.986963270632, 0.002147580696, 0., 
        0.010889148671, 0.026963270632, 0.962147580696, 0., 
        0., 0., 0., 1.
    };
    CreateMatrixOp(ops, &RRT_SAT_MAT[0], TRANSFORM_DIR_FORWARD);            
}

void Generate_tonecurve_ops(OpRcPtrVec & ops)
{
    // Convert to Log space.
    CreateLogOp(ops, 10., TRANSFORM_DIR_FORWARD);

    // Apply RRT shaper using the same quadratic B-spline as the CTL.
    {
        auto curve = GradingBSplineCurve::Create({
                {-5.26017743f, -4.f},
                {-3.75502745f, -3.57868829f},
                {-2.24987747f, -1.82131329f},
                {-0.74472749f,  0.68124124f},
                { 1.06145248f,  2.87457742f},
                { 2.86763245f,  3.83406206f},
                { 4.67381243f,  4.f}
            });
        float slopes[] = { 0.f,  0.55982688f,  1.77532247f,  1.55f,  0.8787017f,  0.18374463f,  0.f };
        for (size_t i = 0; i < 7; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }

    // Apply SDR ODT shaper using the same quadratic B-spline as the CTL.
    {
        auto curve = GradingBSplineCurve::Create({
                {-2.54062362f,  -1.69897000f},
                {-2.08035721f,  -1.58843500f},
                {-1.62009080f,  -1.35350000f},
                {-1.15982439f,  -1.04695000f},
                {-0.69955799f,  -0.65640000f},
                {-0.23929158f,  -0.22141000f},
                { 0.22097483f,   0.22814402f},
                { 0.68124124f,   0.68124124f},
                { 1.01284632f,   0.99142189f},
                { 1.34445140f,   1.25800000f},
                { 1.67605648f,   1.44995000f},
                { 2.00766156f,   1.55910000f},
                { 2.33926665f,   1.62260000f},
                { 2.67087173f,   1.66065457f},
                { 3.00247681f,   1.68124124f}
            });
        float slopes[] = { 0.f,  0.4803088f,   0.5405565f,   0.79149813f,  0.9055625f,  0.98460368f,
                           0.96884766f,  1.f,  0.87078346f,  0.73702127f,  0.42068113f,  0.23763206f,
                           0.14535362f,  0.08416378f,  0.04f };
        for (size_t i = 0; i < 15; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }

    // Undo the logarithm.
    CreateLogOp(ops, 10., TRANSFORM_DIR_INVERSE);

    // Apply Cinema White/Black correction.
    {
        static constexpr double CINEMA_WHITE = 48.;
        // Note: ACESlib.ODT_Common.ctl claims that using pow10(log10(0.02) for black improves performance at 0,
        // but that does not seem to be the case here, 0 input currently gives about 4e-11 XYZ output either way.
        static constexpr double CINEMA_BLACK = 0.02;
        static constexpr double scale        = 1. / (CINEMA_WHITE - CINEMA_BLACK);
        static constexpr double scale4[4]    = { scale, scale, scale, 1. };
        static constexpr double offset       = -CINEMA_BLACK * scale;
        static constexpr double offset4[4]   = { offset, offset, offset, 0. };

        CreateScaleOffsetOp(ops, scale4, offset4, TRANSFORM_DIR_FORWARD);
    }
}

void Generate_video_adjustment_ops(OpRcPtrVec & ops)
{
    // Surround correction for cinema to video.
    CreateFixedFunctionOp(ops, FixedFunctionOpData::ACES_DARK_TO_DIM_10_FWD, {});

    // Desat to compensate 48 nit to 100 nit brightness.
    static constexpr double DESAT_100_NITS[4 * 4]
    {
        0.949056010175, 0.047185723607, 0.003758266219, 0., 
        0.019056010175, 0.977185723607, 0.003758266219, 0., 
        0.019056010175, 0.047185723607, 0.933758266219, 0., 
        0., 0., 0., 1.
    };
    CreateMatrixOp(ops, &DESAT_100_NITS[0], TRANSFORM_DIR_FORWARD);
}

void Generate_hdr_tonecurve_ops(OpRcPtrVec & ops, double Y_MAX)
{
    // Convert to Log space.
    CreateLogOp(ops, 10., TRANSFORM_DIR_FORWARD);

    // Apply RRT shaper using the same quadratic B-spline as the CTL.
    if (Y_MAX == 1000.)
    {
        auto curve = GradingBSplineCurve::Create({
                { -5.60050155f,  -4.00000000f },
                { -4.09535157f,  -3.57868829f },
                { -2.59020159f,  -1.82131329f },
                { -1.08505161f,   0.68124124f },
                {  0.22347059f,   2.22673503f },
                {  1.53199279f,   2.87906206f },
                {  2.84051500f,   3.00000000f }
            });
        const float slopes[] = { 0.f,  0.55982688f,  1.77532247f,  1.55f,  0.81219728f,  0.1848466f,  0.f };
        for (size_t i = 0; i < 7; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }
    else if (Y_MAX == 2000.)
    {
        auto curve = GradingBSplineCurve::Create({
                { -5.59738488f,  -4.00000000f },
                { -4.09223490f,  -3.57868829f },
                { -2.58708492f,  -1.82131329f },
                { -1.08193494f,   0.68124124f },
                {  0.37639718f,   2.42130131f },
                {  1.83472930f,   3.16609199f },
                {  3.29306142f,   3.30103000f },
            });
        const float slopes[] = { 0.f,  0.55982688f,  1.77532247f,  1.55f,  0.83637009f,  0.18505799f,  0.f };
        for (size_t i = 0; i < 7; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }
    else if (Y_MAX == 4000.)
    {
        auto curve = GradingBSplineCurve::Create({
                { -5.59503319f,  -4.00000000f },
                { -4.08988322f,  -3.57868829f },
                { -2.58473324f,  -1.82131329f },
                { -1.07958326f,   0.68124124f },
                {  0.52855878f,   2.61625839f },
                {  2.13670081f,   3.45351273f },
                {  3.74484285f,   3.60205999f },
            });
        const float slopes[] = { 0.f,  0.55982688f,  1.77532247f,  1.55f,  0.85652519f,  0.18474395f,  0.f };
        for (size_t i = 0; i < 7; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }
    else if (Y_MAX == 108.)
    {
        auto curve = GradingBSplineCurve::Create({
                { -5.37852506f,  -4.00000000f },
                { -3.87337508f,  -3.57868829f },
                { -2.36822510f,  -1.82131329f },
                { -0.86307513f,   0.68124124f },
                { -0.03557710f,   1.60464482f },
                {  0.79192092f,   1.96008059f },
                {  1.61941895f,   2.03342376f },
            });
        const float slopes[] = { 0.f,  0.55982688f,  1.77532247f,  1.55f,  0.68179646f,  0.17726487f,  0.f };
        for (size_t i = 0; i < 7; ++i)
        {
            curve->setSlope( i, slopes[i] );
        }
        ConstGradingBSplineCurveRcPtr m = curve;
        auto identity = GradingBSplineCurve::Create({ { 0.f, 0.f }, { 1.f, 1.f } });
        ConstGradingBSplineCurveRcPtr z = identity;
        auto gc = std::make_shared<GradingRGBCurveOpData>(GRADING_LOG, z, z, z, m);

        CreateGradingRGBCurveOp(ops, gc, TRANSFORM_DIR_FORWARD);
    }

    // Undo the logarithm.
    CreateLogOp(ops, 10., TRANSFORM_DIR_INVERSE);

    // Apply Cinema White/Black correction.
    {
        const double Y_MIN        = 0.0001;
        const double scale        = 1. / (Y_MAX - Y_MIN);
        const double scale4[4]    = { scale, scale, scale, 1. };
        const double offset       = -Y_MIN * scale;
        const double offset4[4]   = { offset, offset, offset, 0. };

        CreateScaleOffsetOp(ops, scale4, offset4, TRANSFORM_DIR_FORWARD);
    }
}

void Generate_sdr_primary_clamp_ops(OpRcPtrVec & ops, const Primaries & limitPrimaries)
{
    MatrixOpData::MatrixArrayPtr matrix1
        = build_conversion_matrix(ACES_AP1::primaries, limitPrimaries, ADAPTATION_BRADFORD);
    CreateMatrixOp(ops, matrix1, TRANSFORM_DIR_FORWARD);

    CreateRangeOp(ops, 
                  0., 1.,
                  0., 1.,
                  TRANSFORM_DIR_FORWARD);

    MatrixOpData::MatrixArrayPtr matrix2
        = rgb2xyz_from_xy(limitPrimaries);
    CreateMatrixOp(ops, matrix2, TRANSFORM_DIR_FORWARD);
}

void Generate_hdr_primary_clamp_ops(OpRcPtrVec & ops, const Primaries & limitPrimaries)
{
    MatrixOpData::MatrixArrayPtr matrix1
        = build_conversion_matrix(ACES_AP1::primaries, limitPrimaries, ADAPTATION_NONE);
    CreateMatrixOp(ops, matrix1, TRANSFORM_DIR_FORWARD);

    CreateRangeOp(ops, 
                  0., 1.,
                  0., 1.,
                  TRANSFORM_DIR_FORWARD);

    MatrixOpData::MatrixArrayPtr matrix2
        = rgb2xyz_from_xy(limitPrimaries);
    CreateMatrixOp(ops, matrix2, TRANSFORM_DIR_FORWARD);

    MatrixOpData::MatrixArrayPtr matrix3
        = build_vonkries_adapt(WHITEPOINT::D60_XYZ, WHITEPOINT::D65_XYZ, ADAPTATION_BRADFORD);
    CreateMatrixOp(ops, matrix3, TRANSFORM_DIR_FORWARD);
}

void Generate_nit_normalization_ops(OpRcPtrVec & ops, double nit_level)
{
    // The PQ curve expects nits / 100 as input.  Unnormalize 1.0 to the nit level for the transform
    // and then renormalize to put 100 nits at 1.0.
    const double scale     = nit_level * 0.01;
    const double scale4[4] = { scale, scale, scale, 1. };
    CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);
}

void Generate_roll_white_d60_ops(OpRcPtrVec & ops)
{
    auto GenerateLutValues = [](double in) -> float
    {
        const double new_wht = 0.918;
        const double width = 0.5;
        const double x0 = -1.0;
        const double x1 = x0 + width;
        const double y0 = -new_wht;
        const double y1 = x1;
        const double m1 = (x1 - x0);
        const double a = y0 - y1 + m1;
        const double b = 2. * ( y1 - y0) - m1;
        const double c = y0;
        const double t = (-in - x0) / (x1 - x0);
        double out = 0.0;
        if (t < 0.0)
        {
            out = -(t * b + c);
        }
        else if (t > 1.0)
        {
            out = in;
        }
        else
        {
            out = -(( t * a + b) * t + c);
        }
        return float(out);
    };

    CreateHalfLut(ops, GenerateLutValues);
}

void Generate_roll_white_d65_ops(OpRcPtrVec & ops)
{
    auto GenerateLutValues = [](double in) -> float
    {
        const double new_wht = 0.908;
        const double width = 0.5;
        const double x0 = -1.0;
        const double x1 = x0 + width;
        const double y0 = -new_wht;
        const double y1 = x1;
        const double m1 = (x1 - x0);
        const double a = y0 - y1 + m1;
        const double b = 2. * ( y1 - y0) - m1;
        const double c = y0;
        const double t = (-in - x0) / (x1 - x0);
        double out = 0.0;
        if (t < 0.0)
        {
            out = -(t * b + c);
        }
        else if (t > 1.0)
        {
            out = in;
        }
        else
        {
            out = -(( t * a + b) * t + c);
        }
        return float(out);
    };

    CreateHalfLut(ops, GenerateLutValues);
}

}  // namespace ACES_OUTPUT

namespace ACES2_OUTPUT
{
    void Generate_output_transform(
        OpRcPtrVec & ops,
        float peak_luminance,
        const Primaries & limiting_pri,
        const Primaries & encoding_pri,
        float linear_scale,
        bool scale_white)
    {
        // Clamp to AP1
        MatrixOpData::MatrixArrayPtr matrixToAP1
            = build_conversion_matrix(ACES_AP0::primaries, ACES_AP1::primaries, ADAPTATION_NONE);
        CreateMatrixOp(ops, matrixToAP1, TRANSFORM_DIR_FORWARD);

        const float upperBound = 8.f * (128.f + 768.f * (std::log(peak_luminance / 100.f) / log(10000.f / 100.f)));
        CreateRangeOp(ops,
            0., upperBound,
            0., upperBound,
            TRANSFORM_DIR_FORWARD);

        CreateMatrixOp(ops, matrixToAP1, TRANSFORM_DIR_INVERSE);

        // Display rendering
        CreateFixedFunctionOp(ops, FixedFunctionOpData::ACES_OUTPUT_TRANSFORM_20_FWD, {
            peak_luminance,
            limiting_pri.m_red.m_xy[0], limiting_pri.m_red.m_xy[1],
            limiting_pri.m_grn.m_xy[0], limiting_pri.m_grn.m_xy[1],
            limiting_pri.m_blu.m_xy[0], limiting_pri.m_blu.m_xy[1],
            limiting_pri.m_wht.m_xy[0], limiting_pri.m_wht.m_xy[1],
        });

        // Post transform clamp
        const double normPeakLuminance = peak_luminance / 100.f;
        CreateRangeOp(ops,
            0., normPeakLuminance,
            0., normPeakLuminance,
            TRANSFORM_DIR_FORWARD);

        // White point simulation
        if (scale_white)
        {
            MatrixOpData::MatrixArrayPtr matrixLimToOut
                = build_conversion_matrix(limiting_pri, encoding_pri, ADAPTATION_NONE);

            MatrixOpData::Offsets white(1.f, 1.f, 1.f, 0.f);
            white = matrixLimToOut->inner(white);

            const double scale = 1. / std::max(std::max(white[0], white[1]), white[2]);
            const double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);
        }

        // Linear scale factor
        if (linear_scale != 1.f)
        {
            const double scale = linear_scale;
            const double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);
        }

        MatrixOpData::MatrixArrayPtr matrixToXYZ
            = build_conversion_matrix_to_XYZ_D65(limiting_pri, ADAPTATION_NONE);
        CreateMatrixOp(ops, matrixToXYZ, TRANSFORM_DIR_FORWARD);
    }

} // namespace ACES2_OUTPUT

//
// Create the built-in transforms.
//

namespace ACES
{

// Register all the ACES related built-in tranforms.
void RegisterAll(BuiltinTransformRegistryImpl & registry) noexcept
{
    {
        auto ACES_AP0_to_CIE_XYZ_D65_BFD_Functor = [](OpRcPtrVec & ops)
        {
            // The CIE XYZ space has its conventional normalization (i.e., to illuminant E).
            // A neutral value of [1.,1.,1] in AP0 maps to the XYZ value of D65 ([0.9504..., 1., 1.089...]).
            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix_to_XYZ_D65(ACES_AP0::primaries, ADAPTATION_BRADFORD);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD", 
                            "Convert ACES AP0 primaries to CIE XYZ with a D65 white point with Bradford adaptation",
                            ACES_AP0_to_CIE_XYZ_D65_BFD_Functor);
    }
    {
        auto ACES_AP1_to_CIE_XYZ_D65_BFD_Functor = [](OpRcPtrVec & ops)
        {
            AP1_to_CIE_XYZ_D65::GenerateOps(ops);
        };

        registry.addBuiltin("UTILITY - ACES-AP1_to_CIE-XYZ-D65_BFD", 
                            "Convert ACES AP1 primaries to CIE XYZ with a D65 white point with Bradford adaptation",
                            ACES_AP1_to_CIE_XYZ_D65_BFD_Functor);
    }
    {
        auto ACES_AP1_to_LINEAR_REC709_BFD_Functor = [](OpRcPtrVec & ops)
        {
            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP1::primaries, REC709::primaries, ADAPTATION_BRADFORD);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("UTILITY - ACES-AP1_to_LINEAR-REC709_BFD", 
                            "Convert ACES AP1 primaries to linear Rec.709 primaries with Bradford adaptation",
                            ACES_AP1_to_LINEAR_REC709_BFD_Functor);
    }
    {
        auto ACEScct_LOG_to_LIN_Functor = [](OpRcPtrVec & ops)
        {
            LogOpDataRcPtr log = ACEScct_to_LINEAR::log.clone();
            CreateLogOp(ops, log, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("CURVE - ACEScct-LOG_to_LINEAR", 
                            "Apply the log-to-lin curve used in ACEScct",
                            ACEScct_LOG_to_LIN_Functor);
    }
    {
        auto ACEScct_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            LogOpDataRcPtr log = ACEScct_to_LINEAR::log.clone();
            CreateLogOp(ops, log, TRANSFORM_DIR_FORWARD);

            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP1::primaries, ACES_AP0::primaries, ADAPTATION_NONE);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACEScct_to_ACES2065-1", 
                            "Convert ACEScct to ACES2065-1",
                            ACEScct_to_ACES2065_1_Functor);
    }
    {
        auto ACEScc_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            auto GenerateLutValues = [](double input) -> float
            {
                // The functor input will be [0,1].  Remap this to a wider domain to better capture
                // the full extent of ACEScc.
                static constexpr double IN_MIN = -0.36;
                static constexpr double IN_MAX =  1.50;
                double in = input * (IN_MAX - IN_MIN) + IN_MIN;

                double out = 0.0f;

                if (in < ((9.72 - 15.0) / 17.52))
                {
                    out = (std::pow( 2., in * 17.52 - 9.72) - std::pow( 2., -16.)) * 2.0;
                }
                else
                {
                    out = std::pow( 2., in * 17.52 - 9.72);
                }
                // The CTL clamps at HALF_MAX, but it's better to avoid a slope discontinuity in a LUT.

                return float(out);
            };

            // Allow the LUT to work over a wider input range to better capture the ACEScc extent.
            CreateRangeOp(ops, 
                         -0.36, 1.5, 
                          0.00, 1.0,
                          TRANSFORM_DIR_FORWARD);

            CreateLut(ops, 4096, GenerateLutValues);

            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP1::primaries, ACES_AP0::primaries, ADAPTATION_NONE);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);

            // This helps when the transform is inverted to match the CTL, 
            // which clamps incoming ACES2065-1 values.
            CreateRangeOp(ops, 
                          0.00, RangeOpData::EmptyValue(), // don't clamp high end, 
                          0.00, RangeOpData::EmptyValue(), // don't clamp high end
                          TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACEScc_to_ACES2065-1", 
                            "Convert ACEScc to ACES2065-1",
                            ACEScc_to_ACES2065_1_Functor);
    }
    {
        auto ACEScg_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP1::primaries, ACES_AP0::primaries, ADAPTATION_NONE);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACEScg_to_ACES2065-1",
                            "Convert ACEScg to ACES2065-1",
                            ACEScg_to_ACES2065_1_Functor);
    }
    {
        auto ACESproxy10i_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            CreateRangeOp(ops, 
                          64. / 1023.,                940. / 1023., 
                          (( 64 - 425.) / 50.) - 2.5, ((940 - 425.) / 50.) - 2.5,
                          TRANSFORM_DIR_FORWARD);

            CreateLogOp(ops, 2., TRANSFORM_DIR_INVERSE);

            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP1::primaries, ACES_AP0::primaries, ADAPTATION_NONE);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACESproxy10i_to_ACES2065-1", 
                            "Convert ACESproxy 10i to ACES2065-1",
                            ACESproxy10i_to_ACES2065_1_Functor);
    }
    {
        auto ADX10_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            static constexpr double scale     = 1023. / 500.;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
    
            static constexpr double offset     = -95. / 500.;
            static constexpr double offset4[4] = { offset, offset, offset, 0. };

            // Convert ADX10 values to Channel Dependent Density values.
            CreateScaleOffsetOp(ops, scale4, offset4, TRANSFORM_DIR_FORWARD);

            // Convert to ACES2065-1.
            ADX_to_ACES::GenerateOps(ops);
        };

        registry.addBuiltin("ADX10_to_ACES2065-1",
                            "Convert ADX10 to ACES2065-1",
                            ADX10_to_ACES2065_1_Functor);
    }
    {
        auto ADX16_to_ACES2065_1_Functor = [](OpRcPtrVec & ops)
        {
            static constexpr double scale     = 65535. / 8000.;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
    
            static constexpr double offset     = -1520. / 8000.;
            static constexpr double offset4[4] = { offset, offset, offset, 0. };

            // Convert ADX16 values to Channel Dependent Density values.
            CreateScaleOffsetOp(ops, scale4, offset4, TRANSFORM_DIR_FORWARD);

            // Convert to ACES2065-1.
            ADX_to_ACES::GenerateOps(ops);
        };

        registry.addBuiltin("ADX16_to_ACES2065-1",
                            "Convert ADX16 to ACES2065-1",
                            ADX16_to_ACES2065_1_Functor);
    }
    {
        auto BLUE_LIGHT_FIX_Functor = [](OpRcPtrVec & ops)
        {
            // Note that in CTL, the matrices are stored transposed.
            static constexpr double BLUE_LIGHT_FIX[4 * 4]
            {
                0.9404372683, -0.0183068787,  0.0778696104, 0., 
                0.0083786969,  0.8286599939,  0.1629613092, 0., 
                0.0005471261, -0.0008833746,  1.0003362486, 0., 
                0., 0., 0., 1.
            };
            CreateMatrixOp(ops, &BLUE_LIGHT_FIX[0], TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACES-LMT - BLUE_LIGHT_ARTIFACT_FIX",
                            "LMT for desaturating blue hues to reduce clipping artifacts",
                            BLUE_LIGHT_FIX_Functor);
    }
    {
        auto GAMUT_COMP_13_Functor = [](OpRcPtrVec & ops)
        {
            MatrixOpData::MatrixArrayPtr matrix
                = build_conversion_matrix(ACES_AP0::primaries, ACES_AP1::primaries, ADAPTATION_NONE);

            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);

            CreateFixedFunctionOp(ops, FixedFunctionOpData::ACES_GAMUT_COMP_13_FWD, {
                1.147, 1.264, 1.312, 0.815, 0.803, 0.880, 1.2});

            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_INVERSE);
        };

        registry.addBuiltin("ACES-LMT - ACES 1.3 Reference Gamut Compression",
                            "LMT (applied in ACES2065-1) to compress scene-referred values from common cameras into the AP1 gamut",
                            GAMUT_COMP_13_Functor);
    }

    //
    // ACES OUTPUT TRANSFORMS
    //

    {
        auto ACES2065_1_to_CIE_XYZ_cinema_1_0_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            AP1_to_CIE_XYZ_D65::GenerateOps(ops);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA_1.0", 
                            "Component of ACES Output Transforms for SDR cinema",
                            ACES2065_1_to_CIE_XYZ_cinema_1_0_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_video_1_0_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_video_adjustment_ops(ops);

            AP1_to_CIE_XYZ_D65::GenerateOps(ops);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO_1.0", 
                            "Component of ACES Output Transforms for SDR D65 video",
                            ACES2065_1_to_CIE_XYZ_video_1_0_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_cinema_rec709lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_sdr_primary_clamp_ops(ops, REC709::primaries);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-REC709lim_1.1", 
                            "Component of ACES Output Transforms for SDR cinema",
                            ACES2065_1_to_CIE_XYZ_cinema_rec709lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_video_rec709lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_video_adjustment_ops(ops);

            ACES_OUTPUT::Generate_sdr_primary_clamp_ops(ops, REC709::primaries);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-REC709lim_1.1", 
                            "Component of ACES Output Transforms for SDR D65 video",
                            ACES2065_1_to_CIE_XYZ_video_rec709lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_video_p3lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_video_adjustment_ops(ops);

            ACES_OUTPUT::Generate_sdr_primary_clamp_ops(ops, P3_D65::primaries);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-P3lim_1.1", 
                            "Component of ACES Output Transforms for SDR D65 video",
                            ACES2065_1_to_CIE_XYZ_video_p3lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_cinema_d60sim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            CreateRangeOp(ops, 
                          RangeOpData::EmptyValue(), 1.0,  // don't clamp low end
                          RangeOpData::EmptyValue(), 1.0,  // don't clamp low end
                          TRANSFORM_DIR_FORWARD);

            static constexpr double scale     = 0.964;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);

            MatrixOpData::MatrixArrayPtr matrix
                = rgb2xyz_from_xy(ACES_AP1::primaries);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D60sim-D65_1.1", 
                            "Component of ACES Output Transforms for SDR D65 cinema simulating D60 white",
                            ACES2065_1_to_CIE_XYZ_cinema_d60sim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_video_d60sim_1_0_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            CreateRangeOp(ops, 
                          RangeOpData::EmptyValue(), 1.0,  // don't clamp low end
                          RangeOpData::EmptyValue(), 1.0,  // don't clamp low end
                          TRANSFORM_DIR_FORWARD);

            static constexpr double scale     = 0.955;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);

            ACES_OUTPUT::Generate_video_adjustment_ops(ops);

            MatrixOpData::MatrixArrayPtr matrix
                = rgb2xyz_from_xy(ACES_AP1::primaries);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-VIDEO-D60sim-D65_1.0", 
                            "Component of ACES Output Transforms for SDR D65 video simulating D60 white",
                            ACES2065_1_to_CIE_XYZ_video_d60sim_1_0_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_cinema_d60sim_dci_1_0_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_roll_white_d60_ops(ops);

            CreateRangeOp(ops, 
                          RangeOpData::EmptyValue(), 0.918,  // don't clamp low end
                          RangeOpData::EmptyValue(), 0.918,  // don't clamp low end
                          TRANSFORM_DIR_FORWARD);

            static constexpr double scale     = 0.96;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);

            MatrixOpData::MatrixArrayPtr matrix
                = rgb2xyz_from_xy(ACES_AP1::primaries);
            CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD);

            MatrixOpData::MatrixArrayPtr matrix2
                = build_vonkries_adapt(WHITEPOINT::DCI_XYZ, WHITEPOINT::D65_XYZ, ADAPTATION_BRADFORD);
            CreateMatrixOp(ops, matrix2, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D60sim-DCI_1.0", 
                            "Component of ACES Output Transforms for SDR DCI cinema simulating D60 white",
                            ACES2065_1_to_CIE_XYZ_cinema_d60sim_dci_1_0_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_cinema_d65sim_dci_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_tonecurve_ops(ops);

            ACES_OUTPUT::Generate_roll_white_d65_ops(ops);

            CreateRangeOp(ops, 
                          RangeOpData::EmptyValue(), 0.908,  // don't clamp low end
                          RangeOpData::EmptyValue(), 0.908,  // don't clamp low end
                          TRANSFORM_DIR_FORWARD);

            static constexpr double scale     = 0.9575;
            static constexpr double scale4[4] = { scale, scale, scale, 1. };
            CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD);

            AP1_to_CIE_XYZ_D65::GenerateOps(ops);

            MatrixOpData::MatrixArrayPtr matrix2
                = build_vonkries_adapt(WHITEPOINT::DCI_XYZ, WHITEPOINT::D65_XYZ, ADAPTATION_BRADFORD);
            CreateMatrixOp(ops, matrix2, TRANSFORM_DIR_FORWARD);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-CINEMA-D65sim-DCI_1.1", 
                            "Component of ACES Output Transforms for SDR DCI cinema simulating D65 white",
                            ACES2065_1_to_CIE_XYZ_cinema_d65sim_dci_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_1000nits_rec2020lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 1000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, REC2020::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 1000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-1000nit-15nit-REC2020lim_1.1", 
                            "Component of ACES Output Transforms for 1000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_1000nits_rec2020lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_1000nits_p3lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 1000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, P3_D65::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 1000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-1000nit-15nit-P3lim_1.1", 
                            "Component of ACES Output Transforms for 1000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_1000nits_p3lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_2000nits_rec2020lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 2000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, REC2020::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 2000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-2000nit-15nit-REC2020lim_1.1", 
                            "Component of ACES Output Transforms for 2000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_2000nits_rec2020lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_2000nits_p3lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 2000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, P3_D65::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 2000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-2000nit-15nit-P3lim_1.1", 
                            "Component of ACES Output Transforms for 2000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_2000nits_p3lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_4000nits_rec2020lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 4000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, REC2020::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 4000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-4000nit-15nit-REC2020lim_1.1", 
                            "Component of ACES Output Transforms for 4000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_4000nits_rec2020lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_video_4000nits_p3lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 4000.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, P3_D65::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 4000.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-VIDEO-4000nit-15nit-P3lim_1.1", 
                            "Component of ACES Output Transforms for 4000 nit HDR D65 video",
                            ACES2065_1_to_CIE_XYZ_hdr_video_4000nits_p3lim_1_1_Functor);
    }

    {
        auto ACES2065_1_to_CIE_XYZ_hdr_cinema_108nits_p3lim_1_1_Functor = [](OpRcPtrVec & ops)
        {
            ACES_OUTPUT::Generate_RRT_preamble_ops(ops);

            ACES_OUTPUT::Generate_hdr_tonecurve_ops(ops, 108.);

            ACES_OUTPUT::Generate_hdr_primary_clamp_ops(ops, P3_D65::primaries);

            ACES_OUTPUT::Generate_nit_normalization_ops(ops, 108.);
        };

        registry.addBuiltin("ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-CINEMA-108nit-7.2nit-P3lim_1.1", 
                            "Component of ACES Output Transforms for 108 nit HDR D65 cinema",
                            ACES2065_1_to_CIE_XYZ_hdr_cinema_108nits_p3lim_1_1_Functor);
    }

    //
    // ACES 2 OUTPUT TRANSFORMS
    //

    struct ACES2OutputTransform
    {
        std::string name;
        std::string desc;
        float peak_luminance;
        Primaries limiting_primaries;
        Primaries encoding_primaries;
        float linear_scale;
        bool scale_white;
    };

    const std::vector<ACES2OutputTransform> aces2_output_transforms {
        //
        // D65
        //
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR Rec709",
            100.f,
            REC709::primaries,
            REC709::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR P3-D65",
            100.f,
            P3_D65::primaries,
            P3_D65::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 108 nit HDR P3-D65",
            225.f,  // = 108 * (100/48);
            P3_D65::primaries,
            P3_D65::primaries,
            0.48f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 300 nit HDR P3-D65",
            625.f,  // = 300 * (100/48);
            P3_D65::primaries,
            P3_D65::primaries,
            0.48f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 500 nit HDR P3-D65",
            500.f,
            P3_D65::primaries,
            P3_D65::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 1000 nit HDR P3-D65",
            1000.f,
            P3_D65::primaries,
            P3_D65::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 2000 nit HDR P3-D65",
            2000.f,
            P3_D65::primaries,
            P3_D65::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 4000 nit HDR P3-D65",
            4000.f,
            P3_D65::primaries,
            P3_D65::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020_2.0",
            "Component of ACES 2 Output Transforms for 500 nit HDR Rec2020",
            500.f,
            REC2020::primaries,
            REC2020::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020_2.0",
            "Component of ACES 2 Output Transforms for 1000 nit HDR Rec2020",
            1000.f,
            REC2020::primaries,
            REC2020::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020_2.0",
            "Component of ACES 2 Output Transforms for 2000 nit HDR Rec2020",
            2000.f,
            REC2020::primaries,
            REC2020::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020_2.0",
            "Component of ACES 2 Output Transforms for 4000 nit HDR Rec2020",
            4000.f,
            REC2020::primaries,
            REC2020::primaries,
            1.f,
            false
        },
        //
        // D60
        //
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC709-D65_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR Rec709 simulating D60 white in Rec709",
            100.f,
            REC709_D60::primaries,
            REC709::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR Rec709 simulating D60 white in P3-D65",
            100.f,
            REC709_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-REC709-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR Rec709 simulating D60 white in Rec2020",
            100.f,
            REC709_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR P3-D60 simulating D60 white in P3-D65",
            100.f,
            P3_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - SDR-100nit-P3-D60-in-XYZ-E_2.0",
            "Component of ACES 2 Output Transforms for 100 nit SDR P3-D60 simulating D60 white in XYZ-E",
            100.f,
            P3_D60::primaries,
            CIE_XYZ_ILLUM_E::primaries,
            1.f,
            false
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-108nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 108 nit HDR P3-D60 simulating D60 white in P3-D65",
            225.f,  // = 108 * (100/48);
            P3_D60::primaries,
            P3_D65::primaries,
            0.48f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-300nit-P3-D60-in-XYZ-E_2.0",
            "Component of ACES 2 Output Transforms for 300 nit HDR P3-D60 simulating D60 white in XYZ-E",
            625.f,  // = 300 * (100/48);
            P3_D60::primaries,
            CIE_XYZ_ILLUM_E::primaries,
            0.48f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 500 nit HDR P3-D60 simulating D60 white in P3-D65",
            500.f,
            P3_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 1000 nit HDR P3-D60 simulating D60 white in P3-D65",
            1000.f,
            P3_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 2000 nit HDR P3-D60 simulating D60 white in P3-D65",
            2000.f,
            P3_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-P3-D65_2.0",
            "Component of ACES 2 Output Transforms for 4000 nit HDR P3-D60 simulating D60 white in P3-D65",
            4000.f,
            P3_D60::primaries,
            P3_D65::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-P3-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 500 nit HDR P3-D60 simulating D60 white in Rec2020",
            500.f,
            P3_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-P3-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 1000 nit HDR P3-D60 simulating D60 white in Rec2020",
            1000.f,
            P3_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-P3-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 2000 nit HDR P3-D60 simulating D60 white in Rec2020",
            2000.f,
            P3_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-P3-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 4000 nit HDR P3-D60 simulating D60 white in Rec2020",
            4000.f,
            P3_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-500nit-REC2020-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 500 nit HDR Rec2020 simulating D60 white in Rec2020",
            500.f,
            REC2020_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-1000nit-REC2020-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 1000 nit HDR Rec2020 simulating D60 white in Rec2020",
            1000.f,
            REC2020_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-2000nit-REC2020-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 2000 nit HDR Rec2020 simulating D60 white in Rec2020",
            2000.f,
            REC2020_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        },
        {
            "ACES-OUTPUT - ACES2065-1_to_CIE-XYZ-D65 - HDR-4000nit-REC2020-D60-in-REC2020-D65_2.0",
            "Component of ACES 2 Output Transforms for 4000 nit HDR Rec2020 simulating D60 white in Rec2020",
            4000.f,
            REC2020_D60::primaries,
            REC2020::primaries,
            1.f,
            true
        }
    };

    for (const auto& tr : aces2_output_transforms)
    {
        auto functor = [tr](OpRcPtrVec & ops)
        {
            ACES2_OUTPUT::Generate_output_transform(
                ops,
                tr.peak_luminance,
                tr.limiting_primaries,
                tr.encoding_primaries,
                tr.linear_scale,
                tr.scale_white
            );
        };

        registry.addBuiltin(tr.name.c_str(), tr.desc.c_str(), functor);
    }
}

} // namespace ACES

} // namespace OCIO_NAMESPACE
