As of O, gradients are interpolated in linear space. This unfortunately affects applications that were expecting a certain behavior for the alpha ramp. This change attempts to get the best of both world: better color interpolation (in linear space) and the old alpha interpolation (in gamma space). This is achieved by applying the electro-optical transfer function to the alpha channel; an idea so wrong it would make any graphics programmer worth his salt weep in disgust. As abhorrent this idea might be to me, it also acts as a faint beacon of hope admist the unfathomable darkness that is Android's color management. And if you allow me another misguided metaphor, this change represents the flotsam I can cling onto in the hope to one day reach the bountiful shores of linear blending and accurate color management. Would this change not fix the distress caused by its predecessors, I will have no choice but bow my head in shame until the day I can finally devise an infallible plan. Bug: 33010587 Test: CtsUiRenderingTestCases Change-Id: I5397fefd7944413f2c820e613a5cba50579d4dd5
276 lines
9.1 KiB
C++
276 lines
9.1 KiB
C++
/*
|
|
* Copyright (C) 2010 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 <utils/JenkinsHash.h>
|
|
|
|
#include "Caches.h"
|
|
#include "Debug.h"
|
|
#include "GradientCache.h"
|
|
#include "Properties.h"
|
|
|
|
#include <cutils/properties.h>
|
|
|
|
namespace android {
|
|
namespace uirenderer {
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Functions
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<typename T>
|
|
static inline T min(T a, T b) {
|
|
return a < b ? a : b;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Cache entry
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
hash_t GradientCacheEntry::hash() const {
|
|
uint32_t hash = JenkinsHashMix(0, count);
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
hash = JenkinsHashMix(hash, android::hash_type(colors[i]));
|
|
hash = JenkinsHashMix(hash, android::hash_type(positions[i]));
|
|
}
|
|
return JenkinsHashWhiten(hash);
|
|
}
|
|
|
|
int GradientCacheEntry::compare(const GradientCacheEntry& lhs, const GradientCacheEntry& rhs) {
|
|
int deltaInt = int(lhs.count) - int(rhs.count);
|
|
if (deltaInt != 0) return deltaInt;
|
|
|
|
deltaInt = memcmp(lhs.colors.get(), rhs.colors.get(), lhs.count * sizeof(uint32_t));
|
|
if (deltaInt != 0) return deltaInt;
|
|
|
|
return memcmp(lhs.positions.get(), rhs.positions.get(), lhs.count * sizeof(float));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Constructors/destructor
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
GradientCache::GradientCache(Extensions& extensions)
|
|
: mCache(LruCache<GradientCacheEntry, Texture*>::kUnlimitedCapacity)
|
|
, mSize(0)
|
|
, mMaxSize(Properties::gradientCacheSize)
|
|
, mUseFloatTexture(extensions.hasFloatTextures())
|
|
, mHasNpot(extensions.hasNPot())
|
|
, mHasSRGB(extensions.hasSRGB()) {
|
|
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize);
|
|
|
|
mCache.setOnEntryRemovedListener(this);
|
|
}
|
|
|
|
GradientCache::~GradientCache() {
|
|
mCache.clear();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Size management
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
uint32_t GradientCache::getSize() {
|
|
return mSize;
|
|
}
|
|
|
|
uint32_t GradientCache::getMaxSize() {
|
|
return mMaxSize;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Callbacks
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void GradientCache::operator()(GradientCacheEntry&, Texture*& texture) {
|
|
if (texture) {
|
|
mSize -= texture->objectSize();
|
|
texture->deleteTexture();
|
|
delete texture;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Caching
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
Texture* GradientCache::get(uint32_t* colors, float* positions, int count) {
|
|
GradientCacheEntry gradient(colors, positions, count);
|
|
Texture* texture = mCache.get(gradient);
|
|
|
|
if (!texture) {
|
|
texture = addLinearGradient(gradient, colors, positions, count);
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
void GradientCache::clear() {
|
|
mCache.clear();
|
|
}
|
|
|
|
void GradientCache::getGradientInfo(const uint32_t* colors, const int count,
|
|
GradientInfo& info) {
|
|
uint32_t width = 256 * (count - 1);
|
|
|
|
// If the npot extension is not supported we cannot use non-clamp
|
|
// wrap modes. We therefore find the nearest largest power of 2
|
|
// unless width is already a power of 2
|
|
if (!mHasNpot && (width & (width - 1)) != 0) {
|
|
width = 1 << (32 - __builtin_clz(width));
|
|
}
|
|
|
|
bool hasAlpha = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (((colors[i] >> 24) & 0xff) < 255) {
|
|
hasAlpha = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
info.width = min(width, uint32_t(mMaxTextureSize));
|
|
info.hasAlpha = hasAlpha;
|
|
}
|
|
|
|
Texture* GradientCache::addLinearGradient(GradientCacheEntry& gradient,
|
|
uint32_t* colors, float* positions, int count) {
|
|
|
|
GradientInfo info;
|
|
getGradientInfo(colors, count, info);
|
|
|
|
Texture* texture = new Texture(Caches::getInstance());
|
|
texture->blend = info.hasAlpha;
|
|
texture->generation = 1;
|
|
|
|
// Assume the cache is always big enough
|
|
const uint32_t size = info.width * 2 * bytesPerPixel();
|
|
while (getSize() + size > mMaxSize) {
|
|
LOG_ALWAYS_FATAL_IF(!mCache.removeOldest(),
|
|
"Ran out of things to remove from the cache? getSize() = %" PRIu32
|
|
", size = %" PRIu32 ", mMaxSize = %" PRIu32 ", width = %" PRIu32,
|
|
getSize(), size, mMaxSize, info.width);
|
|
}
|
|
|
|
generateTexture(colors, positions, info.width, 2, texture);
|
|
|
|
mSize += size;
|
|
LOG_ALWAYS_FATAL_IF((int)size != texture->objectSize(),
|
|
"size != texture->objectSize(), size %" PRIu32 ", objectSize %d"
|
|
" width = %" PRIu32 " bytesPerPixel() = %zu",
|
|
size, texture->objectSize(), info.width, bytesPerPixel());
|
|
mCache.put(gradient, texture);
|
|
|
|
return texture;
|
|
}
|
|
|
|
size_t GradientCache::bytesPerPixel() const {
|
|
// We use 4 channels (RGBA)
|
|
return 4 * (mUseFloatTexture ? /* fp16 */ 2 : sizeof(uint8_t));
|
|
}
|
|
|
|
size_t GradientCache::sourceBytesPerPixel() const {
|
|
// We use 4 channels (RGBA) and upload from floats (not half floats)
|
|
return 4 * (mUseFloatTexture ? sizeof(float) : sizeof(uint8_t));
|
|
}
|
|
|
|
void GradientCache::mixBytes(const FloatColor& start, const FloatColor& end,
|
|
float amount, uint8_t*& dst) const {
|
|
float oppAmount = 1.0f - amount;
|
|
float a = start.a * oppAmount + end.a * amount;
|
|
*dst++ = uint8_t(a * OECF_sRGB((start.r * oppAmount + end.r * amount)) * 255.0f);
|
|
*dst++ = uint8_t(a * OECF_sRGB((start.g * oppAmount + end.g * amount)) * 255.0f);
|
|
*dst++ = uint8_t(a * OECF_sRGB((start.b * oppAmount + end.b * amount)) * 255.0f);
|
|
*dst++ = uint8_t(a * 255.0f);
|
|
}
|
|
|
|
void GradientCache::mixFloats(const FloatColor& start, const FloatColor& end,
|
|
float amount, uint8_t*& dst) const {
|
|
float oppAmount = 1.0f - amount;
|
|
float a = start.a * oppAmount + end.a * amount;
|
|
float* d = (float*) dst;
|
|
#ifdef ANDROID_ENABLE_LINEAR_BLENDING
|
|
*d++ = a * (start.r * oppAmount + end.r * amount);
|
|
*d++ = a * (start.g * oppAmount + end.g * amount);
|
|
*d++ = a * (start.b * oppAmount + end.b * amount);
|
|
#else
|
|
// What we're doing to the alpha channel here is technically incorrect
|
|
// but reproduces Android's old behavior when the alpha was pre-multiplied
|
|
// with gamma-encoded colors
|
|
a = EOCF_sRGB(a);
|
|
*d++ = a * OECF_sRGB(start.r * oppAmount + end.r * amount);
|
|
*d++ = a * OECF_sRGB(start.g * oppAmount + end.g * amount);
|
|
*d++ = a * OECF_sRGB(start.b * oppAmount + end.b * amount);
|
|
#endif
|
|
*d++ = a;
|
|
dst += 4 * sizeof(float);
|
|
}
|
|
|
|
void GradientCache::generateTexture(uint32_t* colors, float* positions,
|
|
const uint32_t width, const uint32_t height, Texture* texture) {
|
|
const GLsizei rowBytes = width * sourceBytesPerPixel();
|
|
uint8_t pixels[rowBytes * height];
|
|
|
|
static ChannelMixer gMixers[] = {
|
|
// colors are stored gamma-encoded
|
|
&android::uirenderer::GradientCache::mixBytes,
|
|
// colors are stored in linear (linear blending on)
|
|
// or gamma-encoded (linear blending off)
|
|
&android::uirenderer::GradientCache::mixFloats,
|
|
};
|
|
ChannelMixer mix = gMixers[mUseFloatTexture];
|
|
|
|
FloatColor start;
|
|
start.setUnPreMultipliedSRGB(colors[0]);
|
|
|
|
FloatColor end;
|
|
end.setUnPreMultipliedSRGB(colors[1]);
|
|
|
|
int currentPos = 1;
|
|
float startPos = positions[0];
|
|
float distance = positions[1] - startPos;
|
|
|
|
uint8_t* dst = pixels;
|
|
for (uint32_t x = 0; x < width; x++) {
|
|
float pos = x / float(width - 1);
|
|
if (pos > positions[currentPos]) {
|
|
start = end;
|
|
startPos = positions[currentPos];
|
|
|
|
currentPos++;
|
|
|
|
end.setUnPreMultipliedSRGB(colors[currentPos]);
|
|
distance = positions[currentPos] - startPos;
|
|
}
|
|
|
|
float amount = (pos - startPos) / distance;
|
|
(this->*mix)(start, end, amount, dst);
|
|
}
|
|
|
|
memcpy(pixels + rowBytes, pixels, rowBytes);
|
|
|
|
if (mUseFloatTexture) {
|
|
texture->upload(GL_RGBA16F, width, height, GL_RGBA, GL_FLOAT, pixels);
|
|
} else {
|
|
GLint internalFormat = mHasSRGB ? GL_SRGB8_ALPHA8 : GL_RGBA;
|
|
texture->upload(internalFormat, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
}
|
|
|
|
texture->setFilter(GL_LINEAR);
|
|
texture->setWrap(GL_CLAMP_TO_EDGE);
|
|
}
|
|
|
|
}; // namespace uirenderer
|
|
}; // namespace android
|