Convert bitmaps to sRGB/scRGB when they have a color profile

This change also fixes an issue with RGBA16F bitmaps when modulated
with a color (for instance by setting an alpha on the Paint object).

The color space conversion is currently done entirely in the shader,
by doing these operations in order:

1. Sample the texture
2. Un-premultiply alpha
3. Apply the EOTF
4. Multiply by the 3x3 color space matrix
5. Apply the OETF
6. Premultiply alpha

Optimizations:
- Steps 2 & 6 are skipped for opaque (common) bitmaps
- Step 3 is skipped when the color space's EOTF is close
  to sRGB (Display P3 for instance). Instead, we use
  a hardware sRGB fetch (when the GPU supports it)
- When step 3 is necessary, we use one of four standard
  EOTF implementations, to save cycles when possible:
  + Linear (doesn't do anything)
  + Full parametric (ICC parametric curve type 4 as defined
    in ICC.1:2004-10, section 10.15)
  + Limited parametric (ICC parametric curve type 3)
  + Gamma (ICC parametric curve type 0)

Color space conversion could be done using texture samplers
instead, for instance 3D LUTs, with or without transfer
functions baked in, or 1D LUTs for transfer functions. This
would result in dependent texture fetches which may or may
not be an advantage over an ALU based implementation. The
current solution favor the use of ALUs to save precious
bandwidth.

Test: CtsUiRenderingTests, CtsGraphicsTests
Bug: 32984164
Change-Id: I10bc3db515e13973b45220f129c66b23f0f7f8fe
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index ec8d63ec..fde0e47 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -50,6 +50,7 @@
     service/GraphicsStatsService.cpp \
     thread/TaskManager.cpp \
     utils/Blur.cpp \
+    utils/Color.cpp \
     utils/GLUtils.cpp \
     utils/LinearAllocator.cpp \
     utils/StringUtils.cpp \
diff --git a/libs/hwui/GlLayer.h b/libs/hwui/GlLayer.h
index 20aaf4a..c4f7fe2 100644
--- a/libs/hwui/GlLayer.h
+++ b/libs/hwui/GlLayer.h
@@ -44,7 +44,7 @@
     }
 
     void setSize(uint32_t width, uint32_t height) override {
-        texture.updateSize(width, height, texture.internalFormat(), texture.format(),
+        texture.updateLayout(width, height, texture.internalFormat(), texture.format(),
                 texture.target());
     }
 
diff --git a/libs/hwui/GlopBuilder.cpp b/libs/hwui/GlopBuilder.cpp
index 5cf52c6..3e7a246 100644
--- a/libs/hwui/GlopBuilder.cpp
+++ b/libs/hwui/GlopBuilder.cpp
@@ -605,7 +605,11 @@
         } else {
             mDescription.hasExternalTexture = true;
         }
-        mDescription.hasLinearTexture = mOutGlop->fill.texture.texture->isLinear();
+        Texture* texture = mOutGlop->fill.texture.texture;
+        mDescription.hasLinearTexture = texture->isLinear();
+        mDescription.hasColorSpaceConversion = texture->hasColorSpaceConversion();
+        mDescription.transferFunction = texture->getTransferFunctionType();
+        mDescription.hasTranslucentConversion = texture->blend;
     }
 
     mDescription.hasColors = mOutGlop->mesh.vertices.attribFlags & VertexAttribFlags::Color;
diff --git a/libs/hwui/Program.h b/libs/hwui/Program.h
index 5c8f8e9..2becfcb 100644
--- a/libs/hwui/Program.h
+++ b/libs/hwui/Program.h
@@ -28,6 +28,7 @@
 #include "FloatColor.h"
 #include "Matrix.h"
 #include "Properties.h"
+#include "utils/Color.h"
 
 namespace android {
 namespace uirenderer {
@@ -56,11 +57,11 @@
 #define PROGRAM_KEY_BITMAP_NPOT         0x80
 #define PROGRAM_KEY_BITMAP_EXTERNAL    0x100
 
-#define PROGRAM_KEY_SWAP_SRC_DST      0x2000
-
 #define PROGRAM_KEY_BITMAP_WRAPS_MASK  0x600
 #define PROGRAM_KEY_BITMAP_WRAPT_MASK 0x1800
 
+#define PROGRAM_KEY_SWAP_SRC_DST_SHIFT 13
+
 // Encode the xfermodes on 6 bits
 #define PROGRAM_MAX_XFERMODE 0x1f
 #define PROGRAM_XFERMODE_SHADER_SHIFT 26
@@ -89,6 +90,10 @@
 #define PROGRAM_HAS_GAMMA_CORRECTION 44
 #define PROGRAM_HAS_LINEAR_TEXTURE 45
 
+#define PROGRAM_HAS_COLOR_SPACE_CONVERSION 46
+#define PROGRAM_TRANSFER_FUNCTION 47 // 2 bits for transfer function
+#define PROGRAM_HAS_TRANSLUCENT_CONVERSION 49
+
 ///////////////////////////////////////////////////////////////////////////////
 // Types
 ///////////////////////////////////////////////////////////////////////////////
@@ -105,13 +110,13 @@
  * A ProgramDescription must be used in conjunction with a ProgramCache.
  */
 struct ProgramDescription {
-    enum class ColorFilterMode {
+    enum class ColorFilterMode : int8_t {
         None = 0,
         Matrix,
         Blend
     };
 
-    enum Gradient {
+    enum Gradient : int8_t {
         kGradientLinear = 0,
         kGradientCircular,
         kGradientSweep
@@ -168,6 +173,11 @@
     // Set when sampling an image in linear space
     bool hasLinearTexture;
 
+    bool hasColorSpaceConversion;
+    TransferFunctionType transferFunction;
+    // Indicates whether the bitmap to convert between color spaces is translucent
+    bool hasTranslucentConversion;
+
     /**
      * Resets this description. All fields are reset back to the default
      * values they hold after building a new instance.
@@ -210,6 +220,10 @@
 
         hasGammaCorrection = false;
         hasLinearTexture = false;
+
+        hasColorSpaceConversion = false;
+        transferFunction = TransferFunctionType::None;
+        hasTranslucentConversion = false;
     }
 
     /**
@@ -263,24 +277,27 @@
                 break;
             case ColorFilterMode::Blend:
                 key |= PROGRAM_KEY_COLOR_BLEND;
-                key |= ((int)colorMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_COLOR_OP_SHIFT;
+                key |= ((int) colorMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_COLOR_OP_SHIFT;
                 break;
             case ColorFilterMode::None:
                 break;
         }
-        key |= ((int)framebufferMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_FRAMEBUFFER_SHIFT;
-        if (swapSrcDst) key |= PROGRAM_KEY_SWAP_SRC_DST;
-        if (modulate) key |= programid(0x1) << PROGRAM_MODULATE_SHIFT;
-        if (hasVertexAlpha) key |= programid(0x1) << PROGRAM_HAS_VERTEX_ALPHA_SHIFT;
-        if (useShadowAlphaInterp) key |= programid(0x1) << PROGRAM_USE_SHADOW_ALPHA_INTERP_SHIFT;
-        if (hasExternalTexture) key |= programid(0x1) << PROGRAM_HAS_EXTERNAL_TEXTURE_SHIFT;
-        if (hasTextureTransform) key |= programid(0x1) << PROGRAM_HAS_TEXTURE_TRANSFORM_SHIFT;
-        if (isSimpleGradient) key |= programid(0x1) << PROGRAM_IS_SIMPLE_GRADIENT;
-        if (hasColors) key |= programid(0x1) << PROGRAM_HAS_COLORS;
-        if (hasDebugHighlight) key |= programid(0x1) << PROGRAM_HAS_DEBUG_HIGHLIGHT;
-        if (hasRoundRectClip) key |= programid(0x1) << PROGRAM_HAS_ROUND_RECT_CLIP;
-        if (hasGammaCorrection) key |= programid(0x1) << PROGRAM_HAS_GAMMA_CORRECTION;
-        if (hasLinearTexture) key |= programid(0x1) << PROGRAM_HAS_LINEAR_TEXTURE;
+        key |= ((int) framebufferMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_FRAMEBUFFER_SHIFT;
+        key |= programid(swapSrcDst) << PROGRAM_KEY_SWAP_SRC_DST_SHIFT;
+        key |= programid(modulate) << PROGRAM_MODULATE_SHIFT;
+        key |= programid(hasVertexAlpha) << PROGRAM_HAS_VERTEX_ALPHA_SHIFT;
+        key |= programid(useShadowAlphaInterp) << PROGRAM_USE_SHADOW_ALPHA_INTERP_SHIFT;
+        key |= programid(hasExternalTexture) << PROGRAM_HAS_EXTERNAL_TEXTURE_SHIFT;
+        key |= programid(hasTextureTransform) << PROGRAM_HAS_TEXTURE_TRANSFORM_SHIFT;
+        key |= programid(isSimpleGradient) << PROGRAM_IS_SIMPLE_GRADIENT;
+        key |= programid(hasColors) << PROGRAM_HAS_COLORS;
+        key |= programid(hasDebugHighlight) << PROGRAM_HAS_DEBUG_HIGHLIGHT;
+        key |= programid(hasRoundRectClip) << PROGRAM_HAS_ROUND_RECT_CLIP;
+        key |= programid(hasGammaCorrection) << PROGRAM_HAS_GAMMA_CORRECTION;
+        key |= programid(hasLinearTexture) << PROGRAM_HAS_LINEAR_TEXTURE;
+        key |= programid(hasColorSpaceConversion) << PROGRAM_HAS_COLOR_SPACE_CONVERSION;
+        key |= programid(transferFunction) << PROGRAM_TRANSFER_FUNCTION;
+        key |= programid(hasTranslucentConversion) << PROGRAM_HAS_TRANSLUCENT_CONVERSION;
         return key;
     }
 
diff --git a/libs/hwui/ProgramCache.cpp b/libs/hwui/ProgramCache.cpp
index 38c23e4..1f78e09 100644
--- a/libs/hwui/ProgramCache.cpp
+++ b/libs/hwui/ProgramCache.cpp
@@ -161,17 +161,61 @@
         "uniform vec4 roundRectInnerRectLTRB;\n"
         "uniform float roundRectRadius;\n";
 
-const char* gFS_OETF[2] = {
-         "\nvec4 OETF(const vec4 linear) {\n"
-         "    return linear;\n"
-         "}\n",
-          // We expect linear data to be scRGB so we mirror the gamma function
-         "\nvec4 OETF(const vec4 linear) {"
-          "    return vec4(sign(linear.rgb) * OETF_sRGB(abs(linear.rgb)), linear.a);\n"
-          "}\n",
+const char* gFS_Uniforms_ColorSpaceConversion =
+        // TODO: Should we use a 3D LUT to combine the matrix and transfer functions?
+        // 32x32x32 fp16 LUTs (for scRGB output) are large and heavy to generate...
+        "uniform mat3 colorSpaceMatrix;\n";
+
+const char* gFS_Uniforms_TransferFunction[4] = {
+        // In this order: g, a, b, c, d, e, f
+        // See ColorSpace::TransferParameters
+        // We'll use hardware sRGB conversion as much as possible
+        "",
+        "uniform float transferFunction[7];\n",
+        "uniform float transferFunction[5];\n",
+        "uniform float transferFunctionGamma;\n"
 };
 
-const char* gFS_Transfer_Functions = R"__SHADER__(
+const char* gFS_OETF[2] = {
+        R"__SHADER__(
+        vec4 OETF(const vec4 linear) {
+            return linear;
+        }
+        )__SHADER__",
+        // We expect linear data to be scRGB so we mirror the gamma function
+        R"__SHADER__(
+        vec4 OETF(const vec4 linear) {
+            return vec4(sign(linear.rgb) * OETF_sRGB(abs(linear.rgb)), linear.a);
+        }
+        )__SHADER__"
+};
+
+const char* gFS_ColorConvert[3] = {
+        // Just OETF
+        R"__SHADER__(
+        vec4 colorConvert(const vec4 color) {
+            return OETF(color);
+        }
+        )__SHADER__",
+        // Full color conversion for opaque bitmaps
+        R"__SHADER__(
+        vec4 colorConvert(const vec4 color) {
+            return OETF(vec4(colorSpaceMatrix * EOTF_Parametric(color.rgb), color.a));
+        }
+        )__SHADER__",
+        // Full color conversion for translucent bitmaps
+        // Note: 0.5/256=0.0019
+        R"__SHADER__(
+        vec4 colorConvert(in vec4 color) {
+            color.rgb /= color.a + 0.0019;
+            color = OETF(vec4(colorSpaceMatrix * EOTF_Parametric(color.rgb), color.a));
+            color.rgb *= color.a + 0.0019;
+            return color;
+        }
+        )__SHADER__",
+};
+
+const char* gFS_sRGB_TransferFunctions = R"__SHADER__(
         float OETF_sRGB(const float linear) {
             // IEC 61966-2-1:1999
             return linear <= 0.0031308 ? linear * 12.92 : (pow(linear, 1.0 / 2.4) * 1.055) - 0.055;
@@ -187,12 +231,56 @@
         }
 )__SHADER__";
 
+const char* gFS_TransferFunction[4] = {
+        // Conversion done by the texture unit (sRGB)
+        R"__SHADER__(
+        vec3 EOTF_Parametric(const vec3 x) {
+            return x;
+        }
+        )__SHADER__",
+        // Full transfer function
+        // TODO: We should probably use a 1D LUT (256x1 with texelFetch() since input is 8 bit)
+        // TODO: That would cause 3 dependent texture fetches. Is it worth it?
+        R"__SHADER__(
+        float EOTF_Parametric(float x) {
+            return x <= transferFunction[4]
+                  ? transferFunction[3] * x + transferFunction[6]
+                  : pow(transferFunction[1] * x + transferFunction[2], transferFunction[0])
+                          + transferFunction[5];
+        }
+
+        vec3 EOTF_Parametric(const vec3 x) {
+            return vec3(EOTF_Parametric(x.r), EOTF_Parametric(x.g), EOTF_Parametric(x.b));
+        }
+        )__SHADER__",
+        // Limited transfer function, e = f = 0.0
+        R"__SHADER__(
+        float EOTF_Parametric(float x) {
+            return x <= transferFunction[4]
+                  ? transferFunction[3] * x
+                  : pow(transferFunction[1] * x + transferFunction[2], transferFunction[0]);
+        }
+
+        vec3 EOTF_Parametric(const vec3 x) {
+            return vec3(EOTF_Parametric(x.r), EOTF_Parametric(x.g), EOTF_Parametric(x.b));
+        }
+        )__SHADER__",
+        // Gamma transfer function, e = f = 0.0
+        R"__SHADER__(
+        vec3 EOTF_Parametric(const vec3 x) {
+            return vec3(pow(x.r, transferFunctionGamma),
+                        pow(x.g, transferFunctionGamma),
+                        pow(x.b, transferFunctionGamma));
+        }
+        )__SHADER__"
+};
+
 // Dithering must be done in the quantization space
 // When we are writing to an sRGB framebuffer, we must do the following:
 //     EOTF(OETF(color) + dither)
 // The dithering pattern is generated with a triangle noise generator in the range [-1.0,1.0]
 // TODO: Handle linear fp16 render targets
-const char* gFS_Gradient_Functions = R"__SHADER__(
+const char* gFS_GradientFunctions = R"__SHADER__(
         float triangleNoise(const highp vec2 n) {
             highp vec2 p = fract(n * vec2(5.3987, 5.4421));
             p += dot(p.yx, p.xy + vec2(21.5351, 14.3137));
@@ -200,7 +288,8 @@
             return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
         }
 )__SHADER__";
-const char* gFS_Gradient_Preamble[2] = {
+
+const char* gFS_GradientPreamble[2] = {
         // Linear framebuffer
         R"__SHADER__(
         vec4 dither(const vec4 color) {
@@ -259,9 +348,9 @@
         "    fragColor *= texture2D(baseSampler, vec2(alpha, 0.5)).a;\n";
 const char* gFS_Main_FetchTexture[2] = {
         // Don't modulate
-        "    fragColor = OETF(texture2D(baseSampler, outTexCoords));\n",
+        "    fragColor = colorConvert(texture2D(baseSampler, outTexCoords));\n",
         // Modulate
-        "    fragColor = color * texture2D(baseSampler, outTexCoords);\n"
+        "    fragColor = color * colorConvert(texture2D(baseSampler, outTexCoords));\n"
 };
 const char* gFS_Main_FetchA8Texture[4] = {
         // Don't modulate
@@ -290,9 +379,9 @@
         "    vec4 gradientColor = gradientMix(startColor, endColor, clamp(index - floor(index), 0.0, 1.0));\n"
 };
 const char* gFS_Main_FetchBitmap =
-        "    vec4 bitmapColor = OETF(texture2D(bitmapSampler, outBitmapTexCoords));\n";
+        "    vec4 bitmapColor = colorConvert(texture2D(bitmapSampler, outBitmapTexCoords));\n";
 const char* gFS_Main_FetchBitmapNpot =
-        "    vec4 bitmapColor = OETF(texture2D(bitmapSampler, wrap(outBitmapTexCoords)));\n";
+        "    vec4 bitmapColor = colorConvert(texture2D(bitmapSampler, wrap(outBitmapTexCoords)));\n";
 const char* gFS_Main_BlendShadersBG =
         "    fragColor = blendShaders(gradientColor, bitmapColor)";
 const char* gFS_Main_BlendShadersGB =
@@ -627,6 +716,11 @@
     }
     shader.append(gFS_Uniforms_ColorOp[static_cast<int>(description.colorOp)]);
 
+    if (description.hasColorSpaceConversion) {
+        shader.append(gFS_Uniforms_ColorSpaceConversion);
+    }
+    shader.append(gFS_Uniforms_TransferFunction[static_cast<int>(description.transferFunction)]);
+
     // Generate required functions
     if (description.hasGradient && description.hasBitmap) {
         generateBlend(shader, "blendShaders", description.shadersMode);
@@ -640,16 +734,21 @@
     if (description.useShaderBasedWrap) {
         generateTextureWrap(shader, description.bitmapWrapS, description.bitmapWrapT);
     }
-    if (description.hasGradient || description.hasLinearTexture) {
-        shader.append(gFS_Transfer_Functions);
+    if (description.hasGradient || description.hasLinearTexture
+            || description.hasColorSpaceConversion) {
+        shader.append(gFS_sRGB_TransferFunctions);
     }
     if (description.hasBitmap || ((description.hasTexture || description.hasExternalTexture) &&
             !description.hasAlpha8Texture)) {
-        shader.append(gFS_OETF[description.hasLinearTexture && !mHasLinearBlending]);
+        shader.append(gFS_TransferFunction[static_cast<int>(description.transferFunction)]);
+        shader.append(gFS_OETF[(description.hasLinearTexture || description.hasColorSpaceConversion)
+                && !mHasLinearBlending]);
+        shader.append(gFS_ColorConvert[description.hasColorSpaceConversion
+                ? 1 + description.hasTranslucentConversion : 0]);
     }
     if (description.hasGradient) {
-        shader.append(gFS_Gradient_Functions);
-        shader.append(gFS_Gradient_Preamble[mHasLinearBlending]);
+        shader.append(gFS_GradientFunctions);
+        shader.append(gFS_GradientPreamble[mHasLinearBlending]);
     }
 
     // Begin the shader
diff --git a/libs/hwui/SkiaShader.cpp b/libs/hwui/SkiaShader.cpp
index 4f7f9d7..8a504d4 100644
--- a/libs/hwui/SkiaShader.cpp
+++ b/libs/hwui/SkiaShader.cpp
@@ -216,8 +216,13 @@
     const float width = outData->bitmapTexture->width();
     const float height = outData->bitmapTexture->height();
 
+    Texture* texture = outData->bitmapTexture;
+
     description->hasBitmap = true;
-    description->hasLinearTexture = outData->bitmapTexture->isLinear();
+    description->hasLinearTexture = texture->isLinear();
+    description->hasColorSpaceConversion = texture->hasColorSpaceConversion();
+    description->transferFunction = texture->getTransferFunctionType();
+    description->hasTranslucentConversion = texture->blend;
     description->isShaderBitmapExternal = hwuiBitmap->isHardware();
     // gralloc doesn't support non-clamp modes
     if (hwuiBitmap->isHardware() || (!caches.extensions().hasNPot()
diff --git a/libs/hwui/Texture.cpp b/libs/hwui/Texture.cpp
index cfc2744..8b71086 100644
--- a/libs/hwui/Texture.cpp
+++ b/libs/hwui/Texture.cpp
@@ -17,10 +17,13 @@
 #include "Caches.h"
 #include "Texture.h"
 #include "utils/GLUtils.h"
+#include "utils/MathUtils.h"
 #include "utils/TraceUtils.h"
 
 #include <utils/Log.h>
 
+#include <math/mat4.h>
+
 #include <SkCanvas.h>
 
 namespace android {
@@ -48,12 +51,7 @@
     }
 }
 
-bool Texture::isLinear() const {
-    return mInternalFormat == GL_RGBA16F;
-}
-
 void Texture::setWrapST(GLenum wrapS, GLenum wrapT, bool bindTexture, bool force) {
-
     if (force || wrapS != mWrapS || wrapT != mWrapT) {
         mWrapS = wrapS;
         mWrapT = wrapT;
@@ -94,7 +92,7 @@
     }
 }
 
-bool Texture::updateSize(uint32_t width, uint32_t height, GLint internalFormat,
+bool Texture::updateLayout(uint32_t width, uint32_t height, GLint internalFormat,
         GLint format, GLenum target) {
     if (mWidth == width
             && mHeight == height
@@ -122,7 +120,7 @@
 void Texture::upload(GLint internalFormat, uint32_t width, uint32_t height,
         GLenum format, GLenum type, const void* pixels) {
     GL_CHECKPOINT(MODERATE);
-    bool needsAlloc = updateSize(width, height, internalFormat, format, GL_TEXTURE_2D);
+    bool needsAlloc = updateLayout(width, height, internalFormat, format, GL_TEXTURE_2D);
     if (!mId) {
         glGenTextures(1, &mId);
         needsAlloc = true;
@@ -224,7 +222,6 @@
         *outType = GL_UNSIGNED_BYTE;
         break;
     case kGray_8_SkColorType:
-        // TODO: Handle sRGB
         *outFormat = GL_LUMINANCE;
         *outInternalFormat = GL_LUMINANCE;
         *outType = GL_UNSIGNED_BYTE;
@@ -252,15 +249,14 @@
     return rgbaBitmap;
 }
 
-bool Texture::hasUnsupportedColorType(const SkImageInfo& info, bool hasLinearBlending,
-        SkColorSpace* sRGB) {
-    bool needSRGB = info.colorSpace() == sRGB;
+bool Texture::hasUnsupportedColorType(const SkImageInfo& info, bool hasLinearBlending) {
     return info.colorType() == kARGB_4444_SkColorType
         || info.colorType() == kIndex_8_SkColorType
-        || (info.colorType() == kRGB_565_SkColorType && hasLinearBlending && needSRGB);
+        || (info.colorType() == kRGB_565_SkColorType
+                && hasLinearBlending
+                && info.colorSpace()->isSRGB());
 }
 
-
 void Texture::upload(Bitmap& bitmap) {
     if (!bitmap.readyToDraw()) {
         ALOGE("Cannot generate texture from bitmap");
@@ -284,23 +280,59 @@
         setDefaultParams = true;
     }
 
-    sk_sp<SkColorSpace> sRGB = SkColorSpace::MakeSRGB();
-    bool needSRGB = bitmap.info().colorSpace() == sRGB.get();
+    bool hasLinearBlending = mCaches.extensions().hasLinearBlending();
+    bool needSRGB = transferFunctionCloseToSRGB(bitmap.info().colorSpace());
 
     GLint internalFormat, format, type;
-    colorTypeToGlFormatAndType(mCaches, bitmap.colorType(), needSRGB, &internalFormat, &format, &type);
+    colorTypeToGlFormatAndType(mCaches, bitmap.colorType(),
+            needSRGB && hasLinearBlending, &internalFormat, &format, &type);
+
+    mConnector.reset();
+
+    // RGBA16F is always extended sRGB, alpha masks don't have color profiles
+    if (internalFormat != GL_RGBA16F && internalFormat != GL_ALPHA) {
+        SkColorSpace* colorSpace = bitmap.info().colorSpace();
+        // If the bitmap is sRGB we don't need conversion
+        if (colorSpace != nullptr && !colorSpace->isSRGB()) {
+            SkMatrix44 xyzMatrix(SkMatrix44::kUninitialized_Constructor);
+            if (!colorSpace->toXYZD50(&xyzMatrix)) {
+                ALOGW("Incompatible color space!");
+            } else {
+                SkColorSpaceTransferFn fn;
+                if (!colorSpace->isNumericalTransferFn(&fn)) {
+                    ALOGW("Incompatible color space, no numerical transfer function!");
+                } else {
+                    float data[16];
+                    xyzMatrix.asColMajorf(data);
+
+                    ColorSpace::TransferParameters p =
+                            {fn.fG, fn.fA, fn.fB, fn.fC, fn.fD, fn.fE, fn.fF};
+                    ColorSpace src("Unnamed", mat4f((const float*) &data[0]).upperLeft(), p);
+                    mConnector.reset(new ColorSpaceConnector(src, ColorSpace::sRGB()));
+
+                    // A non-sRGB color space might have a transfer function close enough to sRGB
+                    // that we can save shader instructions by using an sRGB sampler
+                    // This is only possible if we have hardware support for sRGB textures
+                    if (needSRGB && internalFormat == GL_RGBA
+                            && mCaches.extensions().hasSRGB() && !bitmap.isHardware()) {
+                        internalFormat = GL_SRGB8_ALPHA8;
+                    }
+                }
+            }
+        }
+    }
 
     GLenum target = bitmap.isHardware() ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D;
-    needsAlloc |= updateSize(bitmap.width(), bitmap.height(), internalFormat, format, target);
+    needsAlloc |= updateLayout(bitmap.width(), bitmap.height(), internalFormat, format, target);
 
     blend = !bitmap.isOpaque();
     mCaches.textureState().bindTexture(mTarget, mId);
 
     // TODO: Handle sRGB gray bitmaps
-    bool hasLinearBlending = mCaches.extensions().hasLinearBlending();
-    if (CC_UNLIKELY(hasUnsupportedColorType(bitmap.info(), hasLinearBlending, sRGB.get()))) {
+    if (CC_UNLIKELY(hasUnsupportedColorType(bitmap.info(), hasLinearBlending))) {
         SkBitmap skBitmap;
         bitmap.getSkBitmap(&skBitmap);
+        sk_sp<SkColorSpace> sRGB = SkColorSpace::MakeSRGB();
         SkBitmap rgbaBitmap = uploadToN32(skBitmap, hasLinearBlending, std::move(sRGB));
         uploadToTexture(needsAlloc, internalFormat, format, type, rgbaBitmap.rowBytesAsPixels(),
                 rgbaBitmap.bytesPerPixel(), rgbaBitmap.width(),
@@ -333,9 +365,28 @@
     mFormat = format;
     mInternalFormat = internalFormat;
     mTarget = target;
+    mConnector.reset();
     // We're wrapping an existing texture, so don't double count this memory
     notifySizeChanged(0);
 }
 
+TransferFunctionType Texture::getTransferFunctionType() const {
+    if (mConnector.get() != nullptr && mInternalFormat != GL_SRGB8_ALPHA8) {
+        const ColorSpace::TransferParameters& p = mConnector->getSource().getTransferParameters();
+        if (MathUtils::isZero(p.e) && MathUtils::isZero(p.f)) {
+            if (MathUtils::areEqual(p.a, 1.0f) && MathUtils::isZero(p.b)
+                    && MathUtils::isZero(p.c) && MathUtils::isZero(p.d)) {
+                if (MathUtils::areEqual(p.g, 1.0f)) {
+                    return TransferFunctionType::None;
+                }
+                return TransferFunctionType::Gamma;
+            }
+            return TransferFunctionType::Limited;
+        }
+        return TransferFunctionType::Full;
+    }
+    return TransferFunctionType::None;
+}
+
 }; // namespace uirenderer
 }; // namespace android
diff --git a/libs/hwui/Texture.h b/libs/hwui/Texture.h
index e7fbf20..052c018 100644
--- a/libs/hwui/Texture.h
+++ b/libs/hwui/Texture.h
@@ -19,6 +19,13 @@
 
 #include "GpuMemoryTracker.h"
 #include "hwui/Bitmap.h"
+#include "utils/Color.h"
+
+#include <memory>
+
+#include <math/mat3.h>
+
+#include <ui/ColorSpace.h>
 
 #include <GLES2/gl2.h>
 #include <EGL/egl.h>
@@ -42,8 +49,7 @@
 public:
     static SkBitmap uploadToN32(const SkBitmap& bitmap,
             bool hasLinearBlending, sk_sp<SkColorSpace> sRGB);
-    static bool hasUnsupportedColorType(const SkImageInfo& info,
-            bool hasLinearBlending, SkColorSpace* sRGB);
+    static bool hasUnsupportedColorType(const SkImageInfo& info, bool hasLinearBlending);
     static void colorTypeToGlFormatAndType(const Caches& caches, SkColorType colorType,
             bool needSRGB, GLint* outInternalFormat, GLint* outFormat, GLint* outType);
 
@@ -130,9 +136,26 @@
     }
 
     /**
+     * Returns nullptr if this texture does not require color space conversion
+     * to sRGB, or a valid pointer to a ColorSpaceConnector if a conversion
+     * is required.
+     */
+    constexpr const ColorSpaceConnector* getColorSpaceConnector() const {
+        return mConnector.get();
+    }
+
+    constexpr bool hasColorSpaceConversion() const {
+        return mConnector.get() != nullptr;
+    }
+
+    TransferFunctionType getTransferFunctionType() const;
+
+    /**
      * Returns true if this texture uses a linear encoding format.
      */
-    bool isLinear() const;
+    constexpr bool isLinear() const {
+        return mInternalFormat == GL_RGBA16F;
+    }
 
     /**
      * Generation of the backing bitmap,
@@ -171,8 +194,8 @@
     // and external texture wrapper
     friend class GlLayer;
 
-    // Returns true if the size changed, false if it was the same
-    bool updateSize(uint32_t width, uint32_t height, GLint internalFormat,
+    // Returns true if the texture layout (size, format, etc.) changed, false if it was the same
+    bool updateLayout(uint32_t width, uint32_t height, GLint internalFormat,
             GLint format, GLenum target);
     void uploadHardwareBitmapToTexture(GraphicBuffer* buffer);
     void resetCachedParams();
@@ -196,6 +219,8 @@
     GLenum mMagFilter = GL_LINEAR;
 
     Caches& mCaches;
+
+    std::unique_ptr<ColorSpaceConnector> mConnector;
 }; // struct Texture
 
 class AutoTexture {
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 72a9f4e..eed5b24 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -19,6 +19,7 @@
 #include "renderthread/EglManager.h"
 #include "renderthread/RenderThread.h"
 #include "renderthread/RenderProxy.h"
+#include "utils/Color.h"
 
 #include <sys/mman.h>
 
@@ -223,8 +224,7 @@
         return nullptr;
     }
 
-    sk_sp<SkColorSpace> sRGB = SkColorSpace::MakeSRGB();
-    bool needSRGB = skBitmap.info().colorSpace() == sRGB.get();
+    bool needSRGB = uirenderer::transferFunctionCloseToSRGB(skBitmap.info().colorSpace());
     bool hasLinearBlending = caches.extensions().hasLinearBlending();
     GLint format, type, internalFormat;
     uirenderer::Texture::colorTypeToGlFormatAndType(caches, skBitmap.colorType(),
@@ -245,7 +245,8 @@
 
     SkBitmap bitmap;
     if (CC_UNLIKELY(uirenderer::Texture::hasUnsupportedColorType(skBitmap.info(),
-            hasLinearBlending, sRGB.get()))) {
+            hasLinearBlending))) {
+        sk_sp<SkColorSpace> sRGB = SkColorSpace::MakeSRGB();
         bitmap = uirenderer::Texture::uploadToN32(skBitmap, hasLinearBlending, std::move(sRGB));
     } else {
         bitmap = skBitmap;
diff --git a/libs/hwui/renderstate/RenderState.cpp b/libs/hwui/renderstate/RenderState.cpp
index 8bce990..c8833d2 100644
--- a/libs/hwui/renderstate/RenderState.cpp
+++ b/libs/hwui/renderstate/RenderState.cpp
@@ -22,8 +22,11 @@
 #include "renderthread/CanvasContext.h"
 #include "renderthread/EglManager.h"
 #include "utils/GLUtils.h"
+
 #include <algorithm>
 
+#include <ui/ColorSpace.h>
+
 namespace android {
 namespace uirenderer {
 
@@ -359,6 +362,40 @@
             fill.skiaShaderData.bitmapData.bitmapTexture : nullptr;
     const AutoTexture autoCleanup(texture);
 
+    // If we have a shader and a base texture, the base texture is assumed to be an alpha mask
+    // which means the color space conversion applies to the shader's bitmap
+    Texture* colorSpaceTexture = texture != nullptr ? texture : fill.texture.texture;
+    if (colorSpaceTexture != nullptr) {
+        if (colorSpaceTexture->hasColorSpaceConversion()) {
+            const ColorSpaceConnector* connector = colorSpaceTexture->getColorSpaceConnector();
+            glUniformMatrix3fv(fill.program->getUniform("colorSpaceMatrix"), 1,
+                    GL_FALSE, connector->getTransform().asArray());
+        }
+
+        TransferFunctionType transferFunction = colorSpaceTexture->getTransferFunctionType();
+        if (transferFunction != TransferFunctionType::None) {
+            const ColorSpaceConnector* connector = colorSpaceTexture->getColorSpaceConnector();
+            const ColorSpace& source = connector->getSource();
+
+            switch (transferFunction) {
+                case TransferFunctionType::None:
+                    break;
+                case TransferFunctionType::Full:
+                    glUniform1fv(fill.program->getUniform("transferFunction"), 7,
+                            reinterpret_cast<const float*>(&source.getTransferParameters().g));
+                    break;
+                case TransferFunctionType::Limited:
+                    glUniform1fv(fill.program->getUniform("transferFunction"), 5,
+                            reinterpret_cast<const float*>(&source.getTransferParameters().g));
+                    break;
+                case TransferFunctionType::Gamma:
+                    glUniform1f(fill.program->getUniform("transferFunctionGamma"),
+                            source.getTransferParameters().g);
+                    break;
+            }
+        }
+    }
+
     // ------------------------------------
     // ---------- GL state setup ----------
     // ------------------------------------
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
new file mode 100644
index 0000000..7d234b0
--- /dev/null
+++ b/libs/hwui/utils/Color.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Color.h"
+
+#include <cmath>
+
+namespace android {
+namespace uirenderer {
+
+static inline bool almostEqual(float a, float b) {
+    return std::abs(a - b) < 1e-2f;
+}
+
+bool transferFunctionCloseToSRGB(const SkColorSpace* colorSpace) {
+    if (colorSpace == nullptr) return true;
+    if (colorSpace->isSRGB()) return true;
+
+    SkColorSpaceTransferFn transferFunction;
+    if (colorSpace->isNumericalTransferFn(&transferFunction)) {
+        // sRGB transfer function params:
+        const float sRGBParamA = 1 / 1.055f;
+        const float sRGBParamB = 0.055f / 1.055f;
+        const float sRGBParamC = 1 / 12.92f;
+        const float sRGBParamD = 0.04045f;
+        const float sRGBParamE = 0.0f;
+        const float sRGBParamF = 0.0f;
+        const float sRGBParamG = 2.4f;
+
+        // This comparison will catch Display P3
+        return
+                almostEqual(sRGBParamA, transferFunction.fA)
+             && almostEqual(sRGBParamB, transferFunction.fB)
+             && almostEqual(sRGBParamC, transferFunction.fC)
+             && almostEqual(sRGBParamD, transferFunction.fD)
+             && almostEqual(sRGBParamE, transferFunction.fE)
+             && almostEqual(sRGBParamF, transferFunction.fF)
+             && almostEqual(sRGBParamG, transferFunction.fG);
+    }
+
+    return false;
+}
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index 4a27ca2f..9c09660 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -19,6 +19,7 @@
 #include <math.h>
 
 #include <SkColor.h>
+#include <SkColorSpace.h>
 
 namespace android {
 namespace uirenderer {
@@ -82,6 +83,13 @@
     };
     static constexpr int BrightColorsCount = sizeof(BrightColors) / sizeof(Color::Color);
 
+    enum class TransferFunctionType : int8_t {
+        None = 0,
+        Full,
+        Limited,
+        Gamma
+    };
+
     // Opto-electronic conversion function for the sRGB color space
     // Takes a linear sRGB value and converts it to a gamma-encoded sRGB value
     static constexpr float OECF_sRGB(float linear) {
@@ -118,6 +126,11 @@
         return srgb;
 #endif
     }
+
+    // Returns whether the specified color space's transfer function can be
+    // approximated with the native sRGB transfer function. This method
+    // returns true for sRGB, gamma 2.2 and Display P3 for instance
+    bool transferFunctionCloseToSRGB(const SkColorSpace* colorSpace);
 } /* namespace uirenderer */
 } /* namespace android */