From caaaa66e57293e4a6f312649bf472eab84d5c7fe Mon Sep 17 00:00:00 2001 From: Romain Guy Date: Mon, 27 Mar 2017 00:40:21 -0700 Subject: [PATCH] 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 --- libs/hwui/Android.mk | 1 + libs/hwui/GlLayer.h | 2 +- libs/hwui/GlopBuilder.cpp | 6 +- libs/hwui/Program.h | 53 ++++++---- libs/hwui/ProgramCache.cpp | 139 ++++++++++++++++++++++---- libs/hwui/SkiaShader.cpp | 7 +- libs/hwui/Texture.cpp | 89 +++++++++++++---- libs/hwui/Texture.h | 35 ++++++- libs/hwui/hwui/Bitmap.cpp | 7 +- libs/hwui/renderstate/RenderState.cpp | 37 +++++++ libs/hwui/utils/Color.cpp | 58 +++++++++++ libs/hwui/utils/Color.h | 13 +++ 12 files changed, 379 insertions(+), 68 deletions(-) create mode 100644 libs/hwui/utils/Color.cpp diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index ec8d63ecde987..fde0e4760d619 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -50,6 +50,7 @@ hwui_src_files := \ 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 20aaf4a35ac11..c4f7fe2a56b8d 100644 --- a/libs/hwui/GlLayer.h +++ b/libs/hwui/GlLayer.h @@ -44,7 +44,7 @@ public: } 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 5cf52c69f0fd4..3e7a246bb281b 100644 --- a/libs/hwui/GlopBuilder.cpp +++ b/libs/hwui/GlopBuilder.cpp @@ -605,7 +605,11 @@ void GlopBuilder::build() { } 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 5c8f8e93fa3d2..2becfcb709c37 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 @@ namespace uirenderer { #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 @@ namespace uirenderer { #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 @@ typedef uint64_t programid; * 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 @@ struct ProgramDescription { // 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 @@ struct ProgramDescription { hasGammaCorrection = false; hasLinearTexture = false; + + hasColorSpaceConversion = false; + transferFunction = TransferFunctionType::None; + hasTranslucentConversion = false; } /** @@ -263,24 +277,27 @@ struct ProgramDescription { 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 38c23e4babe8d..1f78e09b5a58f 100644 --- a/libs/hwui/ProgramCache.cpp +++ b/libs/hwui/ProgramCache.cpp @@ -161,17 +161,61 @@ const char* gFS_Uniforms_HasRoundRectClip = "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 @@ const char* gFS_Transfer_Functions = R"__SHADER__( } )__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 @@ const char* gFS_Gradient_Functions = R"__SHADER__( 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 @@ const char* gFS_Main_ApplyVertexAlphaShadowInterp = " 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 @@ const char* gFS_Main_FetchGradient[6] = { " 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 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti } shader.append(gFS_Uniforms_ColorOp[static_cast(description.colorOp)]); + if (description.hasColorSpaceConversion) { + shader.append(gFS_Uniforms_ColorSpaceConversion); + } + shader.append(gFS_Uniforms_TransferFunction[static_cast(description.transferFunction)]); + // Generate required functions if (description.hasGradient && description.hasBitmap) { generateBlend(shader, "blendShaders", description.shadersMode); @@ -640,16 +734,21 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti 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(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 4f7f9d7f9b9a3..8a504d4431c4f 100644 --- a/libs/hwui/SkiaShader.cpp +++ b/libs/hwui/SkiaShader.cpp @@ -216,8 +216,13 @@ bool tryStoreBitmap(Caches& caches, const SkShader& shader, const Matrix4& model 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 cfc2744e61b26..8b71086e16258 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 +#include + #include namespace android { @@ -48,12 +51,7 @@ static int bytesPerPixel(GLint glFormat) { } } -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 @@ void Texture::deleteTexture() { } } -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::resetCachedParams() { 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 @@ void Texture::colorTypeToGlFormatAndType(const Caches& caches, SkColorType color *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 @@ SkBitmap Texture::uploadToN32(const SkBitmap& bitmap, bool hasLinearBlending, 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 @@ void Texture::upload(Bitmap& bitmap) { setDefaultParams = true; } - sk_sp 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 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 @@ void Texture::wrap(GLuint id, uint32_t width, uint32_t height, 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 e7fbf20cd8989..052c018903174 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 + +#include + +#include #include #include @@ -42,8 +49,7 @@ class Texture : public GpuMemoryTracker { public: static SkBitmap uploadToN32(const SkBitmap& bitmap, bool hasLinearBlending, sk_sp 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); @@ -129,10 +135,27 @@ public: return mTarget; } + /** + * 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 @@ private: // 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 @@ private: GLenum mMagFilter = GL_LINEAR; Caches& mCaches; + + std::unique_ptr mConnector; }; // struct Texture class AutoTexture { diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 72a9f4e34a2ee..eed5b242c1d2e 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 @@ -223,8 +224,7 @@ sk_sp Bitmap::allocateHardwareBitmap(uirenderer::renderthread::RenderThr return nullptr; } - sk_sp 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 @@ sk_sp Bitmap::allocateHardwareBitmap(uirenderer::renderthread::RenderThr SkBitmap bitmap; if (CC_UNLIKELY(uirenderer::Texture::hasUnsupportedColorType(skBitmap.info(), - hasLinearBlending, sRGB.get()))) { + hasLinearBlending))) { + sk_sp 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 8bce990129de1..c8833d2a74892 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 +#include + namespace android { namespace uirenderer { @@ -359,6 +362,40 @@ void RenderState::render(const Glop& glop, const Matrix4& orthoMatrix) { 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(&source.getTransferParameters().g)); + break; + case TransferFunctionType::Limited: + glUniform1fv(fill.program->getUniform("transferFunction"), 5, + reinterpret_cast(&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 0000000000000..7d234b06b8cad --- /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 + +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 4a27ca2f327a4..9c096601c8261 100644 --- a/libs/hwui/utils/Color.h +++ b/libs/hwui/utils/Color.h @@ -19,6 +19,7 @@ #include #include +#include namespace android { namespace uirenderer { @@ -82,6 +83,13 @@ namespace uirenderer { }; 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 @@ namespace uirenderer { 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 */