Manually generated sRGB mipmaps, with successively smaller draws.

Dirty GL-generated mipmaps whenever an sRGB texture is used with a new
value for TEXTURE_SRGB_DECODE. Add a new test rectangle to the gamma GM
that tests that textures are correctly converted to linear before
filtering when generating mipmaps.

Added a new unit test that alternates how a texture is interpreted (sRGB
or not), to verify that we rebuild mipmaps when needed, and that we get
the correct results out in both modes.

This test originally failed on four of our bots producing incorrect mips
in three different ways. I'm not real surprised, but it looks like
we can't rely on glGenerateMipmap to do the right thing, in conjunction
with TEXTURE_SRGB_DECODE.

Instead, actually create mip-chains using a series of draw calls.
(My first attempt used glBlitFramebuffer, and that still had bugs on
several bots). This approach appears to work correctly on any device
that fully supports sRGB.

Because the mipmap draws are fairly destructive to state, I had to
hoist them out of bindTexture. That means adding a second pass over
the texture accesses in the processor, at the very beginning of flush.

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search2?unt=true&query=source_type%3Dgm&master=false&issue=1840473002

Review-Url: https://codereview.chromium.org/2007973002
This commit is contained in:
brianosman 2016-06-02 05:49:21 -07:00 committed by Commit bot
parent 0851d2d047
commit 33f6b3f6ee
9 changed files with 686 additions and 35 deletions

View File

@ -10,7 +10,7 @@
#include "Resources.h"
#include "SkGradientShader.h"
DEF_SIMPLE_GM(gamma, canvas, 500, 200) {
DEF_SIMPLE_GM(gamma, canvas, 560, 200) {
SkPaint p;
const SkScalar sz = 50.0f;
const int szInt = SkScalarTruncToInt(sz);
@ -34,8 +34,19 @@ DEF_SIMPLE_GM(gamma, canvas, 500, 200) {
SkImageInfo srgbGreyInfo = SkImageInfo::MakeN32(szInt, szInt, kOpaque_SkAlphaType,
kSRGB_SkColorProfileType);
srgbGreyBmp.allocPixels(srgbGreyInfo);
// 0xBC = 255 * linear_to_srgb(0.5f)
srgbGreyBmp.eraseARGB(0xFF, 0xBC, 0xBC, 0xBC);
SkBitmap mipmapBmp;
SkImageInfo mipmapInfo = SkImageInfo::MakeN32(2, 2, kOpaque_SkAlphaType,
kSRGB_SkColorProfileType);
mipmapBmp.allocPixels(mipmapInfo);
SkPMColor* mipmapPixels = reinterpret_cast<SkPMColor*>(mipmapBmp.getPixels());
// 0x89 = 255 * linear_to_srgb(0.25f)
mipmapPixels[0] = mipmapPixels[3] = SkPackARGB32(0xFF, 0x89, 0x89, 0x89);
// 0xE1 = 255 * linear_to_srgb(0.75f)
mipmapPixels[1] = mipmapPixels[2] = SkPackARGB32(0xFF, 0xE1, 0xE1, 0xE1);
SkPaint textPaint;
textPaint.setColor(SK_ColorWHITE);
@ -107,6 +118,12 @@ DEF_SIMPLE_GM(gamma, canvas, 500, 200) {
p.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
nextRect("Dither", "Scale");
// 25%/75% dither, scaled down by 2x. Tests ALL aspects of minification. Specifically, are
// sRGB sources decoded to linear before computing mipmaps?
p.setShader(SkShader::MakeBitmapShader(mipmapBmp, rpt, rpt, &scaleMatrix));
p.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
nextRect("MipMaps", 0);
// 50% grey via paint color.
p.setColor(0xff7f7f7f);
nextRect("Color", 0);

View File

@ -53,7 +53,7 @@ protected:
private:
void computeScratchKey(GrScratchKey*) const override;
size_t onGpuMemorySize() const override;
void dirtyMipMaps(bool mipMapsDirty);
void dirtyMipMaps(bool mipMapsDirty, bool sRGBCorrect);
enum MipMapsStatus {
kNotAllocated_MipMapsStatus,
@ -64,6 +64,7 @@ private:
GrSLType fSamplerType;
MipMapsStatus fMipMapsStatus;
int fMaxMipMapLevel;
bool fMipMapsAreSRGBCorrect;
friend class GrTexturePriv;

View File

@ -18,7 +18,7 @@
#include "SkMipMap.h"
#include "SkTypes.h"
void GrTexture::dirtyMipMaps(bool mipMapsDirty) {
void GrTexture::dirtyMipMaps(bool mipMapsDirty, bool sRGBCorrect) {
if (mipMapsDirty) {
if (kValid_MipMapsStatus == fMipMapsStatus) {
fMipMapsStatus = kAllocated_MipMapsStatus;
@ -26,6 +26,7 @@ void GrTexture::dirtyMipMaps(bool mipMapsDirty) {
} else {
const bool sizeChanged = kNotAllocated_MipMapsStatus == fMipMapsStatus;
fMipMapsStatus = kValid_MipMapsStatus;
fMipMapsAreSRGBCorrect = sRGBCorrect;
if (sizeChanged) {
// This must not be called until after changing fMipMapsStatus.
this->didChangeGpuMemorySize();
@ -93,9 +94,12 @@ GrTexture::GrTexture(GrGpu* gpu, const GrSurfaceDesc& desc, GrSLType samplerType
if (wasMipMapDataProvided) {
fMipMapsStatus = kValid_MipMapsStatus;
fMaxMipMapLevel = SkMipMap::ComputeLevelCount(fDesc.fWidth, fDesc.fHeight);
// At the moment, the CPU code for generating mipmaps doesn't account for sRGB:
fMipMapsAreSRGBCorrect = false;
} else {
fMipMapsStatus = kNotAllocated_MipMapsStatus;
fMaxMipMapLevel = 0;
fMipMapsAreSRGBCorrect = false;
}
}

View File

@ -29,7 +29,9 @@ public:
return 0 != (fTexture->fDesc.fFlags & flags);
}
void dirtyMipMaps(bool mipMapsDirty) { fTexture->dirtyMipMaps(mipMapsDirty); }
void dirtyMipMaps(bool mipMapsDirty, bool sRGBCorrect = false) {
fTexture->dirtyMipMaps(mipMapsDirty, sRGBCorrect);
}
bool mipMapsAreDirty() const {
return GrTexture::kValid_MipMapsStatus != fTexture->fMipMapsStatus;
@ -47,6 +49,10 @@ public:
return fTexture->fMaxMipMapLevel;
}
bool mipMapsAreSRGBCorrect() const {
return fTexture->fMipMapsAreSRGBCorrect;
}
static void ComputeScratchKey(const GrSurfaceDesc&, GrScratchKey*);
private:

View File

@ -201,6 +201,9 @@ GrGLGpu::GrGLGpu(GrGLContext* ctx, GrContext* context)
for (size_t i = 0; i < SK_ARRAY_COUNT(fCopyPrograms); ++i) {
fCopyPrograms[i].fProgram = 0;
}
for (size_t i = 0; i < SK_ARRAY_COUNT(fMipmapPrograms); ++i) {
fMipmapPrograms[i].fProgram = 0;
}
fWireRectProgram.fProgram = 0;
fPLSSetupProgram.fProgram = 0;
@ -257,6 +260,7 @@ GrGLGpu::~GrGLGpu() {
// to release the resources held by the objects themselves.
fPathRendering.reset();
fCopyProgramArrayBuffer.reset();
fMipmapProgramArrayBuffer.reset();
fWireRectArrayBuffer.reset();
fPLSSetupProgram.fArrayBuffer.reset();
@ -282,6 +286,12 @@ GrGLGpu::~GrGLGpu() {
}
}
for (size_t i = 0; i < SK_ARRAY_COUNT(fMipmapPrograms); ++i) {
if (0 != fMipmapPrograms[i].fProgram) {
GL_CALL(DeleteProgram(fMipmapPrograms[i].fProgram));
}
}
if (0 != fWireRectProgram.fProgram) {
GL_CALL(DeleteProgram(fWireRectProgram.fProgram));
}
@ -421,6 +431,11 @@ void GrGLGpu::disconnect(DisconnectType type) {
GL_CALL(DeleteProgram(fCopyPrograms[i].fProgram));
}
}
for (size_t i = 0; i < SK_ARRAY_COUNT(fMipmapPrograms); ++i) {
if (fMipmapPrograms[i].fProgram) {
GL_CALL(DeleteProgram(fMipmapPrograms[i].fProgram));
}
}
if (fWireRectProgram.fProgram) {
GL_CALL(DeleteProgram(fWireRectProgram.fProgram));
}
@ -444,6 +459,10 @@ void GrGLGpu::disconnect(DisconnectType type) {
for (size_t i = 0; i < SK_ARRAY_COUNT(fCopyPrograms); ++i) {
fCopyPrograms[i].fProgram = 0;
}
fMipmapProgramArrayBuffer.reset();
for (size_t i = 0; i < SK_ARRAY_COUNT(fMipmapPrograms); ++i) {
fMipmapPrograms[i].fProgram = 0;
}
fWireRectProgram.fProgram = 0;
fWireRectArrayBuffer.reset();
fPLSSetupProgram.fProgram = 0;
@ -1981,6 +2000,14 @@ void GrGLGpu::flushMinSampleShading(float minSampleShading) {
}
bool GrGLGpu::flushGLState(const GrPipeline& pipeline, const GrPrimitiveProcessor& primProc) {
SkAutoTUnref<GrGLProgram> program(fProgramCache->refProgram(this, pipeline, primProc));
if (!program) {
GrCapsDebugf(this->caps(), "Failed to create program!\n");
return false;
}
program->generateMipmaps(primProc, pipeline);
GrXferProcessor::BlendInfo blendInfo;
pipeline.getXferProcessor().getBlendInfo(&blendInfo);
@ -1988,12 +2015,6 @@ bool GrGLGpu::flushGLState(const GrPipeline& pipeline, const GrPrimitiveProcesso
this->flushDrawFace(pipeline.getDrawFace());
this->flushMinSampleShading(primProc.getSampleShading());
SkAutoTUnref<GrGLProgram> program(fProgramCache->refProgram(this, pipeline, primProc));
if (!program) {
GrCapsDebugf(this->caps(), "Failed to create program!\n");
return false;
}
GrGLuint programID = program->programID();
if (fHWProgramID != programID) {
GL_CALL(UseProgram(programID));
@ -2645,19 +2666,22 @@ void GrGLGpu::flushRenderTarget(GrGLRenderTarget* target, const SkIRect* bounds,
}
if (this->glCaps().srgbWriteControl()) {
bool enableSRGBWrite = GrPixelConfigIsSRGB(target->config()) && !disableSRGB;
if (enableSRGBWrite && kYes_TriState != fHWSRGBFramebuffer) {
GL_CALL(Enable(GR_GL_FRAMEBUFFER_SRGB));
fHWSRGBFramebuffer = kYes_TriState;
} else if (!enableSRGBWrite && kNo_TriState != fHWSRGBFramebuffer) {
GL_CALL(Disable(GR_GL_FRAMEBUFFER_SRGB));
fHWSRGBFramebuffer = kNo_TriState;
}
this->flushFramebufferSRGB(GrPixelConfigIsSRGB(target->config()) && !disableSRGB);
}
this->didWriteToSurface(target, bounds);
}
void GrGLGpu::flushFramebufferSRGB(bool enable) {
if (enable && kYes_TriState != fHWSRGBFramebuffer) {
GL_CALL(Enable(GR_GL_FRAMEBUFFER_SRGB));
fHWSRGBFramebuffer = kYes_TriState;
} else if (!enable && kNo_TriState != fHWSRGBFramebuffer) {
GL_CALL(Disable(GR_GL_FRAMEBUFFER_SRGB));
fHWSRGBFramebuffer = kNo_TriState;
}
}
void GrGLGpu::flushViewport(const GrGLIRect& viewport) {
if (fHWViewport != viewport) {
viewport.pushToGLViewport(this->glInterface());
@ -3138,17 +3162,6 @@ void GrGLGpu::bindTexture(int unitIdx, const GrTextureParams& params, bool allow
bool setAll = timestamp < this->getResetTimestamp();
GrGLTexture::TexParams newTexParams;
if (this->caps()->srgbSupport()) {
// By default, the decision to allow SRGB decode is based on the destination config.
// A texture can override that by specifying a value in GrTextureParams.
newTexParams.fSRGBDecode = allowSRGBInputs ? GR_GL_DECODE_EXT : GR_GL_SKIP_DECODE_EXT;
if (setAll || newTexParams.fSRGBDecode != oldTexParams.fSRGBDecode) {
this->setTextureUnit(unitIdx);
GL_CALL(TexParameteri(target, GR_GL_TEXTURE_SRGB_DECODE_EXT, newTexParams.fSRGBDecode));
}
}
static GrGLenum glMinFilterModes[] = {
GR_GL_NEAREST,
GR_GL_LINEAR,
@ -3170,16 +3183,27 @@ void GrGLGpu::bindTexture(int unitIdx, const GrTextureParams& params, bool allow
newTexParams.fMinFilter = glMinFilterModes[filterMode];
newTexParams.fMagFilter = glMagFilterModes[filterMode];
if (GrTextureParams::kMipMap_FilterMode == filterMode) {
if (texture->texturePriv().mipMapsAreDirty()) {
bool enableSRGBDecode = false;
if (GrPixelConfigIsSRGB(texture->config())) {
enableSRGBDecode = allowSRGBInputs;
newTexParams.fSRGBDecode = enableSRGBDecode ? GR_GL_DECODE_EXT : GR_GL_SKIP_DECODE_EXT;
if (setAll || newTexParams.fSRGBDecode != oldTexParams.fSRGBDecode) {
this->setTextureUnit(unitIdx);
GL_CALL(GenerateMipmap(target));
texture->texturePriv().dirtyMipMaps(false);
texture->texturePriv().setMaxMipMapLevel(SkMipMap::ComputeLevelCount(
texture->width(), texture->height()));
GL_CALL(TexParameteri(target, GR_GL_TEXTURE_SRGB_DECODE_EXT, newTexParams.fSRGBDecode));
}
}
#ifdef SK_DEBUG
// We were supposed to ensure MipMaps were up-to-date and built correctly before getting here.
if (GrTextureParams::kMipMap_FilterMode == filterMode) {
SkASSERT(!texture->texturePriv().mipMapsAreDirty());
if (GrPixelConfigIsSRGB(texture->config())) {
SkASSERT(texture->texturePriv().mipMapsAreSRGBCorrect() == enableSRGBDecode);
}
}
#endif
newTexParams.fMaxMipMapLevel = texture->texturePriv().maxMipMapLevel();
newTexParams.fWrapS = tile_to_gl_wrap(params.getTileModeX());
@ -3278,6 +3302,67 @@ void GrGLGpu::bindTexelBuffer(int unitIdx, intptr_t offsetInBytes, GrPixelConfig
}
}
void GrGLGpu::generateMipmaps(const GrTextureParams& params, bool allowSRGBInputs,
GrGLTexture* texture) {
SkASSERT(texture);
// First, figure out if we need mips for this texture at all:
GrTextureParams::FilterMode filterMode = params.filterMode();
if (GrTextureParams::kMipMap_FilterMode == filterMode) {
if (!this->caps()->mipMapSupport() || GrPixelConfigIsCompressed(texture->config())) {
filterMode = GrTextureParams::kBilerp_FilterMode;
}
}
if (GrTextureParams::kMipMap_FilterMode != filterMode) {
return;
}
// If this is an sRGB texture and the mips were previously built the "other" way
// (gamma-correct vs. not), then we need to rebuild them. We don't need to check for
// srgbSupport - we'll *never* get an sRGB pixel config if we don't support it.
if (GrPixelConfigIsSRGB(texture->config()) &&
allowSRGBInputs != texture->texturePriv().mipMapsAreSRGBCorrect()) {
texture->texturePriv().dirtyMipMaps(true);
}
// If the mips aren't dirty, we're done:
if (!texture->texturePriv().mipMapsAreDirty()) {
return;
}
// If we created a rt/tex and rendered to it without using a texture and now we're texturing
// from the rt it will still be the last bound texture, but it needs resolving.
GrGLRenderTarget* texRT = static_cast<GrGLRenderTarget*>(texture->asRenderTarget());
if (texRT) {
this->onResolveRenderTarget(texRT);
}
GrGLenum target = texture->target();
this->setScratchTextureUnit();
GL_CALL(BindTexture(target, texture->textureID()));
// Configure sRGB decode, if necessary. This state is the only thing needed for the driver
// call (glGenerateMipmap) to work correctly. Our manual method dirties other state, too.
if (GrPixelConfigIsSRGB(texture->config())) {
GL_CALL(TexParameteri(target, GR_GL_TEXTURE_SRGB_DECODE_EXT,
allowSRGBInputs ? GR_GL_DECODE_EXT : GR_GL_SKIP_DECODE_EXT));
}
// Either do manual mipmap generation or (if that fails), just rely on the driver:
if (!this->generateMipmap(texture, allowSRGBInputs)) {
GL_CALL(GenerateMipmap(target));
}
texture->texturePriv().dirtyMipMaps(false, allowSRGBInputs);
texture->texturePriv().setMaxMipMapLevel(SkMipMap::ComputeLevelCount(
texture->width(), texture->height()));
// We have potentially set lots of state on the texture. Easiest to dirty it all:
texture->textureParamsModified();
}
void GrGLGpu::setTextureSwizzle(int unitIdx, GrGLenum target, const GrGLenum swizzle[]) {
this->setTextureUnit(unitIdx);
if (this->glStandard() == kGLES_GrGLStandard) {
@ -3711,6 +3796,167 @@ bool GrGLGpu::createCopyProgram(int progIdx) {
return true;
}
bool GrGLGpu::createMipmapProgram(int progIdx) {
const bool oddWidth = SkToBool(progIdx & 0x2);
const bool oddHeight = SkToBool(progIdx & 0x1);
const int numTaps = (oddWidth ? 2 : 1) * (oddHeight ? 2 : 1);
const GrGLSLCaps* glslCaps = this->glCaps().glslCaps();
SkASSERT(!fMipmapPrograms[progIdx].fProgram);
GL_CALL_RET(fMipmapPrograms[progIdx].fProgram, CreateProgram());
if (!fMipmapPrograms[progIdx].fProgram) {
return false;
}
const char* version = glslCaps->versionDeclString();
GrGLSLShaderVar aVertex("a_vertex", kVec2f_GrSLType, GrShaderVar::kAttribute_TypeModifier);
GrGLSLShaderVar uTexCoordXform("u_texCoordXform", kVec4f_GrSLType,
GrShaderVar::kUniform_TypeModifier);
GrGLSLShaderVar uTexture("u_texture", kSampler2D_GrSLType, GrShaderVar::kUniform_TypeModifier);
// We need 1, 2, or 4 texture coordinates (depending on parity of each dimension):
GrGLSLShaderVar vTexCoords[] = {
GrGLSLShaderVar("v_texCoord0", kVec2f_GrSLType, GrShaderVar::kVaryingOut_TypeModifier),
GrGLSLShaderVar("v_texCoord1", kVec2f_GrSLType, GrShaderVar::kVaryingOut_TypeModifier),
GrGLSLShaderVar("v_texCoord2", kVec2f_GrSLType, GrShaderVar::kVaryingOut_TypeModifier),
GrGLSLShaderVar("v_texCoord3", kVec2f_GrSLType, GrShaderVar::kVaryingOut_TypeModifier),
};
GrGLSLShaderVar oFragColor("o_FragColor", kVec4f_GrSLType,
GrShaderVar::kOut_TypeModifier);
SkString vshaderTxt(version);
if (glslCaps->noperspectiveInterpolationSupport()) {
if (const char* extension = glslCaps->noperspectiveInterpolationExtensionString()) {
vshaderTxt.appendf("#extension %s : require\n", extension);
}
vTexCoords[0].addModifier("noperspective");
vTexCoords[1].addModifier("noperspective");
vTexCoords[2].addModifier("noperspective");
vTexCoords[3].addModifier("noperspective");
}
aVertex.appendDecl(glslCaps, &vshaderTxt);
vshaderTxt.append(";");
uTexCoordXform.appendDecl(glslCaps, &vshaderTxt);
vshaderTxt.append(";");
for (int i = 0; i < numTaps; ++i) {
vTexCoords[i].appendDecl(glslCaps, &vshaderTxt);
vshaderTxt.append(";");
}
vshaderTxt.append(
"// Mipmap Program VS\n"
"void main() {"
" gl_Position.xy = a_vertex * vec2(2, 2) - vec2(1, 1);"
" gl_Position.zw = vec2(0, 1);"
);
// Insert texture coordinate computation:
if (oddWidth && oddHeight) {
vshaderTxt.append(
" v_texCoord0 = a_vertex.xy * u_texCoordXform.yw;"
" v_texCoord1 = a_vertex.xy * u_texCoordXform.yw + vec2(u_texCoordXform.x, 0);"
" v_texCoord2 = a_vertex.xy * u_texCoordXform.yw + vec2(0, u_texCoordXform.z);"
" v_texCoord3 = a_vertex.xy * u_texCoordXform.yw + u_texCoordXform.xz;"
);
} else if (oddWidth) {
vshaderTxt.append(
" v_texCoord0 = a_vertex.xy * vec2(u_texCoordXform.y, 1);"
" v_texCoord1 = a_vertex.xy * vec2(u_texCoordXform.y, 1) + vec2(u_texCoordXform.x, 0);"
);
} else if (oddHeight) {
vshaderTxt.append(
" v_texCoord0 = a_vertex.xy * vec2(1, u_texCoordXform.w);"
" v_texCoord1 = a_vertex.xy * vec2(1, u_texCoordXform.w) + vec2(0, u_texCoordXform.z);"
);
} else {
vshaderTxt.append(
" v_texCoord0 = a_vertex.xy;"
);
}
vshaderTxt.append("}");
SkString fshaderTxt(version);
if (glslCaps->noperspectiveInterpolationSupport()) {
if (const char* extension = glslCaps->noperspectiveInterpolationExtensionString()) {
fshaderTxt.appendf("#extension %s : require\n", extension);
}
}
GrGLSLAppendDefaultFloatPrecisionDeclaration(kDefault_GrSLPrecision, *glslCaps,
&fshaderTxt);
for (int i = 0; i < numTaps; ++i) {
vTexCoords[i].setTypeModifier(GrShaderVar::kVaryingIn_TypeModifier);
vTexCoords[i].appendDecl(glslCaps, &fshaderTxt);
fshaderTxt.append(";");
}
uTexture.appendDecl(glslCaps, &fshaderTxt);
fshaderTxt.append(";");
const char* fsOutName;
if (glslCaps->mustDeclareFragmentShaderOutput()) {
oFragColor.appendDecl(glslCaps, &fshaderTxt);
fshaderTxt.append(";");
fsOutName = oFragColor.c_str();
} else {
fsOutName = "gl_FragColor";
}
const char* sampleFunction = GrGLSLTexture2DFunctionName(kVec2f_GrSLType, kSampler2D_GrSLType,
this->glslGeneration());
fshaderTxt.append(
"// Mipmap Program FS\n"
"void main() {"
);
if (oddWidth && oddHeight) {
fshaderTxt.appendf(
" %s = (%s(u_texture, v_texCoord0) + %s(u_texture, v_texCoord1) + "
" %s(u_texture, v_texCoord2) + %s(u_texture, v_texCoord3)) * 0.25;",
fsOutName, sampleFunction, sampleFunction, sampleFunction, sampleFunction
);
} else if (oddWidth || oddHeight) {
fshaderTxt.appendf(
" %s = (%s(u_texture, v_texCoord0) + %s(u_texture, v_texCoord1)) * 0.5;",
fsOutName, sampleFunction, sampleFunction
);
} else {
fshaderTxt.appendf(
" %s = %s(u_texture, v_texCoord0);",
fsOutName, sampleFunction
);
}
fshaderTxt.append("}");
const char* str;
GrGLint length;
str = vshaderTxt.c_str();
length = SkToInt(vshaderTxt.size());
GrGLuint vshader = GrGLCompileAndAttachShader(*fGLContext, fMipmapPrograms[progIdx].fProgram,
GR_GL_VERTEX_SHADER, &str, &length, 1,
&fStats);
str = fshaderTxt.c_str();
length = SkToInt(fshaderTxt.size());
GrGLuint fshader = GrGLCompileAndAttachShader(*fGLContext, fMipmapPrograms[progIdx].fProgram,
GR_GL_FRAGMENT_SHADER, &str, &length, 1,
&fStats);
GL_CALL(LinkProgram(fMipmapPrograms[progIdx].fProgram));
GL_CALL_RET(fMipmapPrograms[progIdx].fTextureUniform,
GetUniformLocation(fMipmapPrograms[progIdx].fProgram, "u_texture"));
GL_CALL_RET(fMipmapPrograms[progIdx].fTexCoordXformUniform,
GetUniformLocation(fMipmapPrograms[progIdx].fProgram, "u_texCoordXform"));
GL_CALL(BindAttribLocation(fMipmapPrograms[progIdx].fProgram, 0, "a_vertex"));
GL_CALL(DeleteShader(vshader));
GL_CALL(DeleteShader(fshader));
return true;
}
bool GrGLGpu::createWireRectProgram() {
if (!fWireRectArrayBuffer) {
static const GrGLfloat vdata[] = {
@ -4068,6 +4314,162 @@ bool GrGLGpu::copySurfaceAsBlitFramebuffer(GrSurface* dst,
return true;
}
bool gManualMipmaps = true;
// Manual implementation of mipmap generation, to work around driver bugs w/sRGB.
// Uses draw calls to do a series of downsample operations to successive mips.
// If this returns false, then the calling code falls back to using glGenerateMipmap.
bool GrGLGpu::generateMipmap(GrGLTexture* texture, bool gammaCorrect) {
// Global switch for manual mipmap generation:
if (!gManualMipmaps) {
return false;
}
// Mipmaps are only supported on 2D textures:
if (GR_GL_TEXTURE_2D != texture->target()) {
return false;
}
// We need to be able to render to the texture for this to work:
if (!this->caps()->isConfigRenderable(texture->config(), false)) {
return false;
}
// Our iterative downsample requires the ability to limit which level we're sampling:
if (!this->glCaps().mipMapLevelAndLodControlSupport()) {
return false;
}
// If we're mipping an sRGB texture, we need to ensure FB sRGB is correct:
if (GrPixelConfigIsSRGB(texture->config())) {
// If we have write-control, just set the state that we want:
if (this->glCaps().srgbWriteControl()) {
this->flushFramebufferSRGB(gammaCorrect);
} else if (!gammaCorrect) {
// If we don't have write-control we can't do non-gamma-correct mipmapping:
return false;
}
}
int width = texture->width();
int height = texture->height();
int levelCount = SkMipMap::ComputeLevelCount(width, height) + 1;
// Define all mips, if we haven't previously done so:
if (0 == texture->texturePriv().maxMipMapLevel()) {
GrGLenum internalFormat;
GrGLenum externalFormat;
GrGLenum externalType;
if (!this->glCaps().getTexImageFormats(texture->config(), texture->config(),
&internalFormat, &externalFormat, &externalType)) {
return false;
}
for (GrGLint level = 1; level < levelCount; ++level) {
// Define the next mip:
width = SkTMax(1, width / 2);
height = SkTMax(1, height / 2);
GL_ALLOC_CALL(this->glInterface(), TexImage2D(GR_GL_TEXTURE_2D, level, internalFormat,
width, height, 0,
externalFormat, externalType, nullptr));
}
}
// Create (if necessary), then bind temporary FBO:
if (0 == fTempDstFBOID) {
GL_CALL(GenFramebuffers(1, &fTempDstFBOID));
}
GL_CALL(BindFramebuffer(GR_GL_FRAMEBUFFER, fTempDstFBOID));
fHWBoundRenderTargetUniqueID = SK_InvalidUniqueID;
// Bind the texture, to get things configured for filtering.
// We'll be changing our base level further below:
this->setTextureUnit(0);
GrTextureParams params(SkShader::kClamp_TileMode, GrTextureParams::kBilerp_FilterMode);
this->bindTexture(0, params, gammaCorrect, texture);
// Vertex data:
if (!fMipmapProgramArrayBuffer) {
static const GrGLfloat vdata[] = {
0, 0,
0, 1,
1, 0,
1, 1
};
fMipmapProgramArrayBuffer.reset(GrGLBuffer::Create(this, sizeof(vdata),
kVertex_GrBufferType,
kStatic_GrAccessPattern, vdata));
}
if (!fMipmapProgramArrayBuffer) {
return false;
}
fHWVertexArrayState.setVertexArrayID(this, 0);
GrGLAttribArrayState* attribs = fHWVertexArrayState.bindInternalVertexArray(this);
attribs->set(this, 0, fMipmapProgramArrayBuffer, kVec2f_GrVertexAttribType,
2 * sizeof(GrGLfloat), 0);
attribs->disableUnusedArrays(this, 0x1);
// Set "simple" state once:
GrXferProcessor::BlendInfo blendInfo;
blendInfo.reset();
this->flushBlend(blendInfo, GrSwizzle::RGBA());
this->flushColorWrite(true);
this->flushDrawFace(GrPipelineBuilder::kBoth_DrawFace);
this->flushHWAAState(nullptr, false, false);
this->disableScissor();
GrStencilSettings stencil;
stencil.setDisabled();
this->flushStencil(stencil);
// Do all the blits:
width = texture->width();
height = texture->height();
GrGLIRect viewport;
viewport.fLeft = 0;
viewport.fBottom = 0;
for (GrGLint level = 1; level < levelCount; ++level) {
// Get and bind the program for this particular downsample (filter shape can vary):
int progIdx = TextureSizeToMipmapProgramIdx(width, height);
if (!fMipmapPrograms[progIdx].fProgram) {
if (!this->createMipmapProgram(progIdx)) {
SkDebugf("Failed to create mipmap program.\n");
return false;
}
}
GL_CALL(UseProgram(fMipmapPrograms[progIdx].fProgram));
fHWProgramID = fMipmapPrograms[progIdx].fProgram;
// Texcoord uniform is expected to contain (1/w, (w-1)/w, 1/h, (h-1)/h)
const float invWidth = 1.0f / width;
const float invHeight = 1.0f / height;
GL_CALL(Uniform4f(fMipmapPrograms[progIdx].fTexCoordXformUniform,
invWidth, (width - 1) * invWidth, invHeight, (height - 1) * invHeight));
GL_CALL(Uniform1i(fMipmapPrograms[progIdx].fTextureUniform, 0));
// Only sample from previous mip
GL_CALL(TexParameteri(GR_GL_TEXTURE_2D, GR_GL_TEXTURE_BASE_LEVEL, level - 1));
GL_CALL(FramebufferTexture2D(GR_GL_FRAMEBUFFER, GR_GL_COLOR_ATTACHMENT0,
GR_GL_TEXTURE_2D, texture->textureID(), level));
width = SkTMax(1, width / 2);
height = SkTMax(1, height / 2);
viewport.fWidth = width;
viewport.fHeight = height;
this->flushViewport(viewport);
GL_CALL(DrawArrays(GR_GL_TRIANGLE_STRIP, 0, 4));
}
// Unbind:
GL_CALL(FramebufferTexture2D(GR_GL_FRAMEBUFFER, GR_GL_COLOR_ATTACHMENT0,
GR_GL_TEXTURE_2D, 0, 0));
return true;
}
void GrGLGpu::onGetMultisampleSpecs(GrRenderTarget* rt,
const GrStencilSettings& stencil,
int* effectiveSampleCnt,

View File

@ -62,6 +62,8 @@ public:
void bindTexelBuffer(int unitIdx, intptr_t offsetInBytes, GrPixelConfig, GrGLBuffer*);
void generateMipmaps(const GrTextureParams& params, bool allowSRGBInputs, GrGLTexture* texture);
bool onGetReadPixelsInfo(GrSurface* srcSurface, int readWidth, int readHeight, size_t rowBytes,
GrPixelConfig readConfig, DrawPreference*,
ReadPixelTempDrawInfo*) override;
@ -237,6 +239,7 @@ private:
GrSurface* src,
const SkIRect& srcRect,
const SkIPoint& dstPoint);
bool generateMipmap(GrGLTexture* texture, bool gammaCorrect);
void stampPLSSetupRect(const SkRect& bounds);
@ -319,6 +322,8 @@ private:
void flushMinSampleShading(float minSampleShading);
void flushFramebufferSRGB(bool enable);
// helper for onCreateTexture and writeTexturePixels
enum UploadType {
kNewTexture_UploadType, // we are creating a new texture
@ -365,6 +370,7 @@ private:
SkAutoTUnref<GrGLContext> fGLContext;
bool createCopyProgram(int progIdx);
bool createMipmapProgram(int progIdx);
bool createWireRectProgram();
bool createPLSSetupProgram();
@ -532,6 +538,14 @@ private:
} fCopyPrograms[3];
SkAutoTUnref<GrGLBuffer> fCopyProgramArrayBuffer;
/** IDs for texture mipmap program. (4 filter configurations) */
struct {
GrGLuint fProgram;
GrGLint fTextureUniform;
GrGLint fTexCoordXformUniform;
} fMipmapPrograms[4];
SkAutoTUnref<GrGLBuffer> fMipmapProgramArrayBuffer;
struct {
GrGLuint fProgram;
GrGLint fColorUniform;
@ -553,6 +567,12 @@ private:
}
}
static int TextureSizeToMipmapProgramIdx(int width, int height) {
const bool wide = (width > 1) && SkToBool(width & 0x1);
const bool tall = (height > 1) && SkToBool(height & 0x1);
return (wide ? 0x2 : 0x0) | (tall ? 0x1 : 0x0);
}
struct {
GrGLuint fProgram;
GrGLint fPosXformUniform;

View File

@ -83,6 +83,23 @@ void GrGLProgram::setData(const GrPrimitiveProcessor& primProc, const GrPipeline
}
}
void GrGLProgram::generateMipmaps(const GrPrimitiveProcessor& primProc,
const GrPipeline& pipeline) {
this->generateMipmaps(primProc, pipeline.getAllowSRGBInputs());
int numProcessors = fFragmentProcessors.count();
for (int i = 0; i < numProcessors; ++i) {
const GrFragmentProcessor& processor = pipeline.getFragmentProcessor(i);
this->generateMipmaps(processor, pipeline.getAllowSRGBInputs());
}
if (primProc.getPixelLocalStorageState() !=
GrPixelLocalStorageState::kDraw_GrPixelLocalStorageState) {
const GrXferProcessor& xp = pipeline.getXferProcessor();
this->generateMipmaps(xp, pipeline.getAllowSRGBInputs());
}
}
void GrGLProgram::setFragmentData(const GrPrimitiveProcessor& primProc,
const GrPipeline& pipeline,
int* nextSamplerIdx) {
@ -146,3 +163,12 @@ void GrGLProgram::bindTextures(const GrProcessor& processor,
static_cast<GrGLBuffer*>(access.buffer()));
}
}
void GrGLProgram::generateMipmaps(const GrProcessor& processor,
bool allowSRGBInputs) {
for (int i = 0; i < processor.numTextures(); ++i) {
const GrTextureAccess& access = processor.textureAccess(i);
fGpu->generateMipmaps(access.getParams(), allowSRGBInputs,
static_cast<GrGLTexture*>(access.getTexture()));
}
}

View File

@ -96,6 +96,12 @@ public:
*/
void setData(const GrPrimitiveProcessor&, const GrPipeline&);
/**
* This function retrieves the textures that need to be used by each GrGL*Processor, and
* ensures that any textures requiring mipmaps have their mipmaps correctly built.
*/
void generateMipmaps(const GrPrimitiveProcessor&, const GrPipeline&);
protected:
typedef GrGLSLProgramDataManager::UniformHandle UniformHandle;
typedef GrGLProgramDataManager::UniformInfoArray UniformInfoArray;
@ -122,6 +128,9 @@ protected:
// Helper for setData() that binds textures and texel buffers to the appropriate texture units
void bindTextures(const GrProcessor&, bool allowSRGBInputs, int* nextSamplerIdx);
// Helper for generateMipmaps() that ensures mipmaps are up to date
void generateMipmaps(const GrProcessor&, bool allowSRGBInputs);
// these reflect the current values of uniforms (GL uniform values travel with program)
RenderTargetState fRenderTargetState;
BuiltinUniformHandles fBuiltinUniformHandles;

166
tests/SRGBMipMapTest.cpp Normal file
View File

@ -0,0 +1,166 @@
/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "Test.h"
#if SK_SUPPORT_GPU
#include "GrCaps.h"
#include "GrContext.h"
#include "GrDrawContext.h"
#include "SkCanvas.h"
#include "SkSurface.h"
// using anonymous namespace because these functions are used as template params.
namespace {
/** convert 0..1 srgb value to 0..1 linear */
float srgb_to_linear(float srgb) {
if (srgb <= 0.04045f) {
return srgb / 12.92f;
} else {
return powf((srgb + 0.055f) / 1.055f, 2.4f);
}
}
/** convert 0..1 linear value to 0..1 srgb */
float linear_to_srgb(float linear) {
if (linear <= 0.0031308) {
return linear * 12.92f;
} else {
return 1.055f * powf(linear, 1.f / 2.4f) - 0.055f;
}
}
}
static bool check_value(U8CPU value, U8CPU expected, U8CPU error) {
if (value >= expected) {
return (value - expected) <= error;
} else {
return (expected - value) <= error;
}
}
void read_and_check_pixels(skiatest::Reporter* reporter, GrTexture* texture, U8CPU expected,
U8CPU error, const char* subtestName) {
int w = texture->width();
int h = texture->height();
SkAutoTMalloc<uint32_t> readData(w * h);
memset(readData.get(), 0, sizeof(uint32_t) * w * h);
if (!texture->readPixels(0, 0, w, h, texture->config(), readData.get())) {
ERRORF(reporter, "Could not read pixels for %s.", subtestName);
return;
}
for (int j = 0; j < h; ++j) {
for (int i = 0; i < w; ++i) {
uint32_t read = readData[j * w + i];
bool success =
check_value(read & 0xff, expected, error) &&
check_value((read >> 8) & 0xff, expected, error) &&
check_value((read >> 16) & 0xff, expected, error);
if (!success) {
ERRORF(reporter, "Expected 0xff%02x%02x%02x, read back as 0x%08x in %s at %d, %d.",
expected, expected, expected, read, subtestName, i, j);
return;
}
}
}
}
DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(SRGBMipMaps, reporter, ctxInfo) {
GrContext* context = ctxInfo.grContext();
if (!context->caps()->srgbSupport()) {
return;
}
const int rtS = 16;
const int texS = rtS * 2;
// Fill texture with a dither of black and 60% sRGB (~ 32.5% linear) gray. Although there is
// only one likely failure mode (doing a direct downsample of the sRGB values), this pattern
// maximizes the minimum error across all three conceivable failure modes:
// 1) Likely incorrect:
// (A + B) / 2
// 2) No input decode, decode output:
// linear_to_srgb((A + B) / 2)
// 3) Decode input, no output encode:
// (srgb_to_linear(A) + srgb_to_linear(B)) / 2
const U8CPU srgb60 = sk_float_round2int(0.6f * 255.0f);
static const SkPMColor colors[2] = {
SkPackARGB32(0xFF, srgb60, srgb60, srgb60),
SkPackARGB32(0xFF, 0x00, 0x00, 0x00)
};
uint32_t texData[texS * texS];
for (int y = 0; y < texS; ++y) {
for (int x = 0; x < texS; ++x) {
texData[y * texS + x] = colors[(x + y) % 2];
}
}
// We can be pretty generous with the error detection, thanks to the choice of input.
// The closest likely failure mode is off by > 0.1, so anything that encodes within
// 10/255 of optimal is more than good enough for this test.
const U8CPU expectedSRGB = sk_float_round2int(
linear_to_srgb(srgb_to_linear(srgb60 / 255.0f) / 2.0f) * 255.0f);
const U8CPU expectedLinear = srgb60 / 2;
const U8CPU error = 10;
// Create our test texture
GrSurfaceDesc desc;
desc.fFlags = kNone_GrSurfaceFlags;
desc.fConfig = kSkiaGamma8888_GrPixelConfig;
desc.fWidth = texS;
desc.fHeight = texS;
GrTextureProvider* texProvider = context->textureProvider();
SkAutoTUnref<GrTexture> texture(texProvider->createTexture(desc, SkBudgeted::kNo, texData, 0));
// Create two surfaces (L32 and S32)
GrSurfaceDesc l32Desc;
l32Desc.fFlags = kRenderTarget_GrSurfaceFlag;
l32Desc.fConfig = kSkia8888_GrPixelConfig;
l32Desc.fWidth = rtS;
l32Desc.fHeight = rtS;
GrSurfaceDesc s32Desc = l32Desc;
s32Desc.fConfig = kSkiaGamma8888_GrPixelConfig;
SkAutoTUnref<GrTexture> l32Texture(texProvider->createTexture(l32Desc, SkBudgeted::kNo));
SkAutoTUnref<GrTexture> s32Texture(texProvider->createTexture(s32Desc, SkBudgeted::kNo));
SkSurfaceProps l32Props(SkSurfaceProps::kLegacyFontHost_InitType);
SkSurfaceProps s32Props(SkSurfaceProps::kGammaCorrect_Flag,
SkSurfaceProps::kLegacyFontHost_InitType);
sk_sp<GrDrawContext> l32DrawContext(
context->drawContext(sk_ref_sp(l32Texture->asRenderTarget()), &l32Props));
sk_sp<GrDrawContext> s32DrawContext(
context->drawContext(sk_ref_sp(s32Texture->asRenderTarget()), &s32Props));
SkRect rect = SkRect::MakeWH(SkIntToScalar(rtS), SkIntToScalar(rtS));
GrNoClip noClip;
GrPaint paint;
paint.setPorterDuffXPFactory(SkXfermode::kSrc_Mode);
GrTextureParams mipMapParams(SkShader::kRepeat_TileMode, GrTextureParams::kMipMap_FilterMode);
paint.addColorTextureProcessor(texture, SkMatrix::MakeScale(0.5f), mipMapParams);
// 1) Draw texture to S32 surface (should generate/use sRGB mips)
paint.setGammaCorrect(true);
s32DrawContext->drawRect(noClip, paint, SkMatrix::I(), rect);
read_and_check_pixels(reporter, s32Texture, expectedSRGB, error, "first render of sRGB");
// 2) Draw texture to L32 surface (should generate/use linear mips)
paint.setGammaCorrect(false);
l32DrawContext->drawRect(noClip, paint, SkMatrix::I(), rect);
read_and_check_pixels(reporter, l32Texture, expectedLinear, error, "re-render as linear");
// 3) Go back to sRGB
paint.setGammaCorrect(true);
s32DrawContext->drawRect(noClip, paint, SkMatrix::I(), rect);
read_and_check_pixels(reporter, s32Texture, expectedSRGB, error, "re-render as sRGB");
}
#endif