Starting to hack up HDR transfer function support

Brings over skcms' encoding scheme, etc.

Change-Id: Ib8abec911acd1c50df3b201b4a9bde01b1cb123b
Bug: chromium:960620
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/249000
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
diff --git a/src/core/SkColorSpace.cpp b/src/core/SkColorSpace.cpp
index bac4b88..4e77d7c 100644
--- a/src/core/SkColorSpace.cpp
+++ b/src/core/SkColorSpace.cpp
@@ -38,7 +38,7 @@
 
 sk_sp<SkColorSpace> SkColorSpace::MakeRGB(const skcms_TransferFunction& transferFn,
                                           const skcms_Matrix3x3& toXYZ) {
-    if (!is_valid_transfer_fn(transferFn)) {
+    if (classify_transfer_fn(transferFn) == Bad_TF) {
         return nullptr;
     }
 
@@ -116,8 +116,11 @@
 }
 
 bool SkColorSpace::isNumericalTransferFn(skcms_TransferFunction* coeffs) const {
+    // TODO: Change transferFn/invTransferFn to just operate on skcms_TransferFunction (all callers
+    // already pass pointers to an skcms struct). Then remove this function, and update the two
+    // remaining callers to do the right thing with transferFn and classify.
     this->transferFn(&coeffs->g);
-    return true;
+    return classify_transfer_fn(*coeffs) == sRGBish_TF;
 }
 
 void SkColorSpace::transferFn(float gabcdef[7]) const {
diff --git a/src/core/SkColorSpacePriv.h b/src/core/SkColorSpacePriv.h
index 97d52e9..425357d 100644
--- a/src/core/SkColorSpacePriv.h
+++ b/src/core/SkColorSpacePriv.h
@@ -31,54 +31,37 @@
     return SkTAbs(a - b) < 0.001f;
 }
 
-static inline bool is_valid_transfer_fn(const skcms_TransferFunction& coeffs) {
-    if (SkScalarIsNaN(coeffs.a) || SkScalarIsNaN(coeffs.b) ||
-        SkScalarIsNaN(coeffs.c) || SkScalarIsNaN(coeffs.d) ||
-        SkScalarIsNaN(coeffs.e) || SkScalarIsNaN(coeffs.f) ||
-        SkScalarIsNaN(coeffs.g))
-    {
-        return false;
-    }
+// NOTE: All of this logic is copied from skcms.cc, and needs to be kept in sync.
 
-    if (coeffs.d < 0.0f) {
-        return false;
-    }
+// Most transfer functions we work with are sRGBish.
+// For exotic HDR transfer functions, we encode them using a tf.g that makes no sense,
+// and repurpose the other fields to hold the parameters of the HDR functions.
+enum TFKind { Bad_TF, sRGBish_TF, PQish_TF, HLGish_TF, HLGinvish_TF };
 
-    if (coeffs.d == 0.0f) {
-        // Y = (aX + b)^g + e  for always
-        if (0.0f == coeffs.a || 0.0f == coeffs.g) {
-            SkColorSpacePrintf("A or G is zero, constant transfer function "
-                               "is nonsense");
-            return false;
+static inline TFKind classify_transfer_fn(const skcms_TransferFunction& tf) {
+    if (tf.g < 0 && (int)tf.g == tf.g) {
+        // TODO: sanity checks for PQ/HLG like we do for sRGBish.
+        switch (-(int)tf.g) {
+            case PQish_TF:     return PQish_TF;
+            case HLGish_TF:    return HLGish_TF;
+            case HLGinvish_TF: return HLGinvish_TF;
         }
+        return Bad_TF;
     }
 
-    if (coeffs.d >= 1.0f) {
-        // Y = cX + f          for always
-        if (0.0f == coeffs.c) {
-            SkColorSpacePrintf("C is zero, constant transfer function is "
-                               "nonsense");
-            return false;
-        }
+    // Basic sanity checks for sRGBish transfer functions.
+    if (sk_float_isfinite(tf.a + tf.b + tf.c + tf.d + tf.e + tf.f + tf.g)
+            // a,c,d,g should be non-negative to make any sense.
+            && tf.a >= 0
+            && tf.c >= 0
+            && tf.d >= 0
+            && tf.g >= 0
+            // Raising a negative value to a fractional tf->g produces complex numbers.
+            && tf.a * tf.d + tf.b >= 0) {
+        return sRGBish_TF;
     }
 
-    if ((0.0f == coeffs.a || 0.0f == coeffs.g) && 0.0f == coeffs.c) {
-        SkColorSpacePrintf("A or G, and C are zero, constant transfer function "
-                           "is nonsense");
-        return false;
-    }
-
-    if (coeffs.c < 0.0f) {
-        SkColorSpacePrintf("Transfer function must be increasing");
-        return false;
-    }
-
-    if (coeffs.a < 0.0f || coeffs.g < 0.0f) {
-        SkColorSpacePrintf("Transfer function must be positive or increasing");
-        return false;
-    }
-
-    return true;
+    return Bad_TF;
 }
 
 static inline bool is_almost_srgb(const skcms_TransferFunction& coeffs) {
diff --git a/src/core/SkColorSpaceXformSteps.cpp b/src/core/SkColorSpaceXformSteps.cpp
index 9c45121..fe03bca 100644
--- a/src/core/SkColorSpaceXformSteps.cpp
+++ b/src/core/SkColorSpaceXformSteps.cpp
@@ -114,12 +114,9 @@
         rgba[2] *= invA;
     }
     if (flags.linearize) {
-        skcms_TransferFunction tf;
-        memcpy(&tf, &srcTF, 7*sizeof(float));
-
-        rgba[0] = skcms_TransferFunction_eval(&tf, rgba[0]);
-        rgba[1] = skcms_TransferFunction_eval(&tf, rgba[1]);
-        rgba[2] = skcms_TransferFunction_eval(&tf, rgba[2]);
+        rgba[0] = skcms_TransferFunction_eval(&srcTF, rgba[0]);
+        rgba[1] = skcms_TransferFunction_eval(&srcTF, rgba[1]);
+        rgba[2] = skcms_TransferFunction_eval(&srcTF, rgba[2]);
     }
     if (flags.gamut_transform) {
         float temp[3] = { rgba[0], rgba[1], rgba[2] };
@@ -130,12 +127,9 @@
         }
     }
     if (flags.encode) {
-        skcms_TransferFunction tf;
-        memcpy(&tf, &dstTFInv, 7*sizeof(float));
-
-        rgba[0] = skcms_TransferFunction_eval(&tf, rgba[0]);
-        rgba[1] = skcms_TransferFunction_eval(&tf, rgba[1]);
-        rgba[2] = skcms_TransferFunction_eval(&tf, rgba[2]);
+        rgba[0] = skcms_TransferFunction_eval(&dstTFInv, rgba[0]);
+        rgba[1] = skcms_TransferFunction_eval(&dstTFInv, rgba[1]);
+        rgba[2] = skcms_TransferFunction_eval(&dstTFInv, rgba[2]);
     }
     if (flags.premul) {
         rgba[0] *= rgba[3];
@@ -152,15 +146,8 @@
     if (flags.linearize) {
         if (src_is_normalized && srcTF_is_sRGB) {
             p->append(SkRasterPipeline::from_srgb);
-        } else if (srcTF.a == 1 &&
-                   srcTF.b == 0 &&
-                   srcTF.c == 0 &&
-                   srcTF.d == 0 &&
-                   srcTF.e == 0 &&
-                   srcTF.f == 0) {
-            p->append(SkRasterPipeline::gamma_, &srcTF.g);
         } else {
-            p->append(SkRasterPipeline::parametric, &srcTF);
+            p->append_transfer_function(srcTF);
         }
     }
     if (flags.gamut_transform) {
@@ -169,15 +156,8 @@
     if (flags.encode) {
         if (src_is_normalized && dstTF_is_sRGB) {
             p->append(SkRasterPipeline::to_srgb);
-        } else if (dstTFInv.a == 1 &&
-                   dstTFInv.b == 0 &&
-                   dstTFInv.c == 0 &&
-                   dstTFInv.d == 0 &&
-                   dstTFInv.e == 0 &&
-                   dstTFInv.f == 0) {
-            p->append(SkRasterPipeline::gamma_, &dstTFInv.g);
         } else {
-            p->append(SkRasterPipeline::parametric, &dstTFInv);
+            p->append_transfer_function(dstTFInv);
         }
     }
     if (flags.premul) { p->append(SkRasterPipeline::premul); }
diff --git a/src/core/SkICC.cpp b/src/core/SkICC.cpp
index 2a2d413..78e4a90 100644
--- a/src/core/SkICC.cpp
+++ b/src/core/SkICC.cpp
@@ -293,7 +293,8 @@
 
 sk_sp<SkData> SkWriteICCProfile(const skcms_TransferFunction& fn,
                                 const skcms_Matrix3x3& toXYZD50) {
-    if (!is_valid_transfer_fn(fn)) {
+    // We can't encode HDR transfer functions in ICC
+    if (classify_transfer_fn(fn) != sRGBish_TF) {
         return nullptr;
     }
 
diff --git a/src/core/SkRasterPipeline.cpp b/src/core/SkRasterPipeline.cpp
index 2e26b47..4f73643 100644
--- a/src/core/SkRasterPipeline.cpp
+++ b/src/core/SkRasterPipeline.cpp
@@ -5,6 +5,7 @@
  * found in the LICENSE file.
  */
 
+#include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkOpts.h"
 #include "src/core/SkRasterPipeline.h"
 #include <algorithm>
@@ -24,6 +25,11 @@
     SkASSERT(stage !=                 set_rgb);  // Please use append_set_rgb().
     SkASSERT(stage !=       unbounded_set_rgb);  // Please use append_set_rgb().
     SkASSERT(stage !=             clamp_gamut);  // Please use append_gamut_clamp_if_normalized().
+    SkASSERT(stage !=              parametric);  // Please use append_transfer_function().
+    SkASSERT(stage !=                  gamma_);  // Please use append_transfer_function().
+    SkASSERT(stage !=                   PQish);  // Please use append_transfer_function().
+    SkASSERT(stage !=                  HLGish);  // Please use append_transfer_function().
+    SkASSERT(stage !=               HLGinvish);  // Please use append_transfer_function().
     this->unchecked_append(stage, ctx);
 }
 void SkRasterPipeline::unchecked_append(StockStage stage, void* ctx) {
@@ -267,6 +273,24 @@
     }
 }
 
+void SkRasterPipeline::append_transfer_function(const skcms_TransferFunction& tf) {
+    void* ctx = const_cast<void*>(static_cast<const void*>(&tf));
+    switch (classify_transfer_fn(tf)) {
+        case Bad_TF: SkASSERT(false); break;
+
+        case TFKind::sRGBish_TF:
+            if (tf.a == 1 && tf.b == 0 && tf.c == 0 && tf.d == 0 && tf.e == 0 && tf.f == 0) {
+                this->unchecked_append(gamma_, ctx);
+            } else {
+                this->unchecked_append(parametric, ctx);
+            }
+            break;
+        case PQish_TF:     this->unchecked_append(PQish,     ctx); break;
+        case HLGish_TF:    this->unchecked_append(HLGish,    ctx); break;
+        case HLGinvish_TF: this->unchecked_append(HLGinvish, ctx); break;
+    }
+}
+
 void SkRasterPipeline::append_gamut_clamp_if_normalized(const SkImageInfo& dstInfo) {
     // N.B. we _do_ clamp for kRGBA_F16Norm_SkColorType... because it's normalized.
     if (dstInfo.colorType() != kRGBA_F16_SkColorType &&
diff --git a/src/core/SkRasterPipeline.h b/src/core/SkRasterPipeline.h
index fab9540..d13a8f0 100644
--- a/src/core/SkRasterPipeline.h
+++ b/src/core/SkRasterPipeline.h
@@ -74,7 +74,7 @@
     M(matrix_translate) M(matrix_scale_translate)                  \
     M(matrix_2x3) M(matrix_3x3) M(matrix_3x4) M(matrix_4x5) M(matrix_4x3) \
     M(matrix_perspective)                                          \
-    M(parametric) M(gamma_)                                        \
+    M(parametric) M(gamma_) M(PQish) M(HLGish) M(HLGinvish)        \
     M(mirror_x)   M(repeat_x)                                      \
     M(mirror_y)   M(repeat_y)                                      \
     M(decal_x)    M(decal_y)   M(decal_x_and_y)                    \
@@ -261,6 +261,8 @@
 
     void append_gamut_clamp_if_normalized(const SkImageInfo&);
 
+    void append_transfer_function(const skcms_TransferFunction&);
+
     bool empty() const { return fStages == nullptr; }
 
 private:
diff --git a/src/effects/SkHighContrastFilter.cpp b/src/effects/SkHighContrastFilter.cpp
index 8a5263f..5382a03 100644
--- a/src/effects/SkHighContrastFilter.cpp
+++ b/src/effects/SkHighContrastFilter.cpp
@@ -72,7 +72,7 @@
         // TODO: sRGB?
         *tf = {2,1, 0,0,0,0,0};
     }
-    p->append(SkRasterPipeline::parametric, tf);
+    p->append_transfer_function(*tf);
 
     if (fConfig.fGrayscale) {
         float r = SK_LUM_COEFF_R;
@@ -120,7 +120,7 @@
         // See above... historically untagged == gamma 2 in this filter.
         *invTF ={0.5f,1, 0,0,0,0,0};
     }
-    p->append(SkRasterPipeline::parametric, invTF);
+    p->append_transfer_function(*invTF);
 
     if (!shaderIsOpaque) {
         p->append(SkRasterPipeline::premul);
diff --git a/src/gpu/GrColorSpaceXform.h b/src/gpu/GrColorSpaceXform.h
index 6abcef1..65e1836 100644
--- a/src/gpu/GrColorSpaceXform.h
+++ b/src/gpu/GrColorSpaceXform.h
@@ -9,6 +9,7 @@
 #define GrColorSpaceXform_DEFINED
 
 #include "include/core/SkRefCnt.h"
+#include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkColorSpaceXformSteps.h"
 #include "src/gpu/GrFragmentProcessor.h"
 
@@ -31,8 +32,19 @@
      * computed key.
      */
     static uint32_t XformKey(const GrColorSpaceXform* xform) {
-        // Code generation depends on which steps we apply
-        return xform ? xform->fSteps.flags.mask() : 0;
+        // Code generation depends on which steps we apply,
+        // and the kinds of transfer functions (if we're applying those).
+        if (!xform) { return 0; }
+
+        const SkColorSpaceXformSteps& steps(xform->fSteps);
+        uint32_t key = steps.flags.mask();
+        if (steps.flags.linearize) {
+            key |= classify_transfer_fn(steps.srcTF)    << 8;
+        }
+        if (steps.flags.encode) {
+            key |= classify_transfer_fn(steps.dstTFInv) << 16;
+        }
+        return key;
     }
 
     static bool Equals(const GrColorSpaceXform* a, const GrColorSpaceXform* b);
diff --git a/src/gpu/glsl/GrGLSLColorSpaceXformHelper.h b/src/gpu/glsl/GrGLSLColorSpaceXformHelper.h
index 52a9eda..1e68897 100644
--- a/src/gpu/glsl/GrGLSLColorSpaceXformHelper.h
+++ b/src/gpu/glsl/GrGLSLColorSpaceXformHelper.h
@@ -8,6 +8,7 @@
 #ifndef GrGLSLColorSpaceXformHelper_DEFINED
 #define GrGLSLColorSpaceXformHelper_DEFINED
 
+#include "src/core/SkColorSpacePriv.h"
 #include "src/core/SkColorSpaceXformSteps.h"
 #include "src/gpu/GrColorSpaceXform.h"
 #include "src/gpu/glsl/GrGLSLUniformHandler.h"
@@ -31,6 +32,7 @@
             if (this->applySrcTF()) {
                 fSrcTFVar = uniformHandler->addUniformArray(visibility, kHalf_GrSLType,
                                                             "SrcTF", kNumTransferFnCoeffs);
+                fSrcTFKind = classify_transfer_fn(colorSpaceXform->fSteps.srcTF);
             }
             if (this->applyGamutXform()) {
                 fGamutXformVar = uniformHandler->addUniform(visibility, kHalf3x3_GrSLType,
@@ -39,6 +41,7 @@
             if (this->applyDstTF()) {
                 fDstTFVar = uniformHandler->addUniformArray(visibility, kHalf_GrSLType,
                                                             "DstTF", kNumTransferFnCoeffs);
+                fDstTFKind = classify_transfer_fn(colorSpaceXform->fSteps.dstTFInv);
             }
         }
     }
@@ -63,6 +66,9 @@
     bool applyDstTF() const      { return fFlags.encode; }
     bool applyPremul() const     { return fFlags.premul; }
 
+    TFKind srcTFKind() const { return fSrcTFKind; }
+    TFKind dstTFKind() const { return fDstTFKind; }
+
     GrGLSLProgramDataManager::UniformHandle srcTFUniform() const { return fSrcTFVar; }
     GrGLSLProgramDataManager::UniformHandle gamutXformUniform() const { return fGamutXformVar; }
     GrGLSLProgramDataManager::UniformHandle dstTFUniform() const { return fDstTFVar; }
@@ -74,6 +80,8 @@
     GrGLSLProgramDataManager::UniformHandle fGamutXformVar;
     GrGLSLProgramDataManager::UniformHandle fDstTFVar;
     SkColorSpaceXformSteps::Flags fFlags;
+    TFKind fSrcTFKind;
+    TFKind fDstTFKind;
 };
 
 #endif
diff --git a/src/gpu/glsl/GrGLSLShaderBuilder.cpp b/src/gpu/glsl/GrGLSLShaderBuilder.cpp
index a87d591..7675744 100644
--- a/src/gpu/glsl/GrGLSLShaderBuilder.cpp
+++ b/src/gpu/glsl/GrGLSLShaderBuilder.cpp
@@ -112,11 +112,13 @@
     // function, one for the (inverse) destination transfer function, and one for the gamut xform.
     // Any combination of these may be present, although some configurations are much more likely.
 
-    auto emitTFFunc = [=](const char* name, GrGLSLProgramDataManager::UniformHandle uniform) {
+    auto emitTFFunc = [=](const char* name, GrGLSLProgramDataManager::UniformHandle uniform,
+                          TFKind kind) {
         const GrShaderVar gTFArgs[] = { GrShaderVar("x", kHalf_GrSLType) };
         const char* coeffs = uniformHandler->getUniformCStr(uniform);
         SkString body;
-        // Temporaries to make evaluation line readable
+        // Temporaries to make evaluation line readable. We always use the sRGBish names, so the
+        // PQ and HLG math is confusing.
         body.appendf("half G = %s[0];", coeffs);
         body.appendf("half A = %s[1];", coeffs);
         body.appendf("half B = %s[2];", coeffs);
@@ -126,7 +128,24 @@
         body.appendf("half F = %s[6];", coeffs);
         body.append("half s = sign(x);");
         body.append("x = abs(x);");
-        body.appendf("return s * ((x < D) ? (C * x) + F : pow(A * x + B, G) + E);");
+        switch (kind) {
+            case TFKind::sRGBish_TF:
+                body.append("x = (x < D) ? (C * x) + F : pow(A * x + B, G) + E;");
+                break;
+            case TFKind::PQish_TF:
+                body.append("x = pow(max(A + B * pow(x, C), 0) / (D + E * pow(x, C)), F);");
+                break;
+            case TFKind::HLGish_TF:
+                body.append("x = (x*A <= 1) ? pow(x*A, B) : exp((x-E)*C) + D;");
+                break;
+            case TFKind::HLGinvish_TF:
+                body.append("x = (x <= 1) ? A * pow(x, B) : C * log(x - D) + E;");
+                break;
+            default:
+                SkASSERT(false);
+                break;
+        }
+        body.append("return s * x;");
         SkString funcName;
         this->emitFunction(kHalf_GrSLType, name, SK_ARRAY_COUNT(gTFArgs), gTFArgs, body.c_str(),
                            &funcName);
@@ -135,12 +154,14 @@
 
     SkString srcTFFuncName;
     if (colorXformHelper->applySrcTF()) {
-        srcTFFuncName = emitTFFunc("src_tf", colorXformHelper->srcTFUniform());
+        srcTFFuncName = emitTFFunc("src_tf", colorXformHelper->srcTFUniform(),
+                                   colorXformHelper->srcTFKind());
     }
 
     SkString dstTFFuncName;
     if (colorXformHelper->applyDstTF()) {
-        dstTFFuncName = emitTFFunc("dst_tf", colorXformHelper->dstTFUniform());
+        dstTFFuncName = emitTFFunc("dst_tf", colorXformHelper->dstTFUniform(),
+                                   colorXformHelper->dstTFKind());
     }
 
     SkString gamutXformFuncName;
diff --git a/src/opts/SkRasterPipeline_opts.h b/src/opts/SkRasterPipeline_opts.h
index e01b029..7d68fa4 100644
--- a/src/opts/SkRasterPipeline_opts.h
+++ b/src/opts/SkRasterPipeline_opts.h
@@ -957,6 +957,12 @@
          -   1.498030302f * m
          -   1.725879990f / (0.3520887068f + m);
 }
+
+SI F approx_log(F x) {
+    const float ln2 = 0.69314718f;
+    return ln2 * approx_log2(x);
+}
+
 SI F approx_pow2(F x) {
     F f = fract(x);
     return bit_cast<F>(round(1.0f * (1<<23),
@@ -965,6 +971,11 @@
                                +  27.728023300f / (4.84252568f - f)));
 }
 
+SI F approx_exp(F x) {
+    const float log2_e = 1.4426950408889634074f;
+    return approx_pow2(log2_e * x);
+}
+
 SI F approx_powf(F x, F y) {
 #if defined(SK_LEGACY_APPROX_POWF_SPECIALCASE)
     return if_then_else((x == 0)         , 0
@@ -1846,6 +1857,58 @@
     b = fn(b);
 }
 
+STAGE(PQish, const skcms_TransferFunction* ctx) {
+    auto fn = [&](F v) {
+        U32 sign;
+        v = strip_sign(v, &sign);
+
+        F r = approx_powf(max(mad(ctx->b, approx_powf(v, ctx->c), ctx->a), 0)
+                           / (mad(ctx->e, approx_powf(v, ctx->c), ctx->d)),
+                        ctx->f);
+
+        return apply_sign(r, sign);
+    };
+    r = fn(r);
+    g = fn(g);
+    b = fn(b);
+}
+
+STAGE(HLGish, const skcms_TransferFunction* ctx) {
+    auto fn = [&](F v) {
+        U32 sign;
+        v = strip_sign(v, &sign);
+
+        const float R = ctx->a, G = ctx->b,
+                    a = ctx->c, b = ctx->d, c = ctx->e;
+
+        F r = if_then_else(v*R <= 1, approx_powf(v*R, G)
+                                   , approx_exp((v-c)*a) + b);
+
+        return apply_sign(r, sign);
+    };
+    r = fn(r);
+    g = fn(g);
+    b = fn(b);
+}
+
+STAGE(HLGinvish, const skcms_TransferFunction* ctx) {
+    auto fn = [&](F v) {
+        U32 sign;
+        v = strip_sign(v, &sign);
+
+        const float R = ctx->a, G = ctx->b,
+                    a = ctx->c, b = ctx->d, c = ctx->e;
+
+        F r = if_then_else(v <= 1, R * approx_powf(v, G)
+                                 , a * approx_log(v - b) + c);
+
+        return apply_sign(r, sign);
+    };
+    r = fn(r);
+    g = fn(g);
+    b = fn(b);
+}
+
 STAGE(from_srgb, Ctx::None) {
     auto fn = [](F s) {
         U32 sign;
@@ -4236,6 +4299,9 @@
     NOT_IMPLEMENTED(matrix_4x3)  // TODO
     NOT_IMPLEMENTED(parametric)
     NOT_IMPLEMENTED(gamma_)
+    NOT_IMPLEMENTED(PQish)
+    NOT_IMPLEMENTED(HLGish)
+    NOT_IMPLEMENTED(HLGinvish)
     NOT_IMPLEMENTED(rgb_to_hsl)
     NOT_IMPLEMENTED(hsl_to_rgb)
     NOT_IMPLEMENTED(gauss_a_to_rgba)  // TODO