From 33f6b3f6ee4de24282f5e7f2dc31a5f538bcf40c Mon Sep 17 00:00:00 2001 From: brianosman Date: Thu, 2 Jun 2016 05:49:21 -0700 Subject: [PATCH] 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 --- gm/gamma.cpp | 19 +- include/gpu/GrTexture.h | 3 +- src/gpu/GrTexture.cpp | 6 +- src/gpu/GrTexturePriv.h | 8 +- src/gpu/gl/GrGLGpu.cpp | 464 ++++++++++++++++++++++++++++++++++--- src/gpu/gl/GrGLGpu.h | 20 ++ src/gpu/gl/GrGLProgram.cpp | 26 +++ src/gpu/gl/GrGLProgram.h | 9 + tests/SRGBMipMapTest.cpp | 166 +++++++++++++ 9 files changed, 686 insertions(+), 35 deletions(-) create mode 100644 tests/SRGBMipMapTest.cpp diff --git a/gm/gamma.cpp b/gm/gamma.cpp index 5dbfb9425b..25608d26b3 100644 --- a/gm/gamma.cpp +++ b/gm/gamma.cpp @@ -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(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); diff --git a/include/gpu/GrTexture.h b/include/gpu/GrTexture.h index 1aa2cbd8e1..2eccd575ef 100644 --- a/include/gpu/GrTexture.h +++ b/include/gpu/GrTexture.h @@ -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; diff --git a/src/gpu/GrTexture.cpp b/src/gpu/GrTexture.cpp index 033f1da891..9209dbd053 100644 --- a/src/gpu/GrTexture.cpp +++ b/src/gpu/GrTexture.cpp @@ -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; } } diff --git a/src/gpu/GrTexturePriv.h b/src/gpu/GrTexturePriv.h index fee7ed1f96..762890fcc0 100644 --- a/src/gpu/GrTexturePriv.h +++ b/src/gpu/GrTexturePriv.h @@ -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: diff --git a/src/gpu/gl/GrGLGpu.cpp b/src/gpu/gl/GrGLGpu.cpp index 6daf955170..1744e25036 100644 --- a/src/gpu/gl/GrGLGpu.cpp +++ b/src/gpu/gl/GrGLGpu.cpp @@ -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 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 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(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, diff --git a/src/gpu/gl/GrGLGpu.h b/src/gpu/gl/GrGLGpu.h index 0b2198a834..06d4bd357b 100644 --- a/src/gpu/gl/GrGLGpu.h +++ b/src/gpu/gl/GrGLGpu.h @@ -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 fGLContext; bool createCopyProgram(int progIdx); + bool createMipmapProgram(int progIdx); bool createWireRectProgram(); bool createPLSSetupProgram(); @@ -532,6 +538,14 @@ private: } fCopyPrograms[3]; SkAutoTUnref fCopyProgramArrayBuffer; + /** IDs for texture mipmap program. (4 filter configurations) */ + struct { + GrGLuint fProgram; + GrGLint fTextureUniform; + GrGLint fTexCoordXformUniform; + } fMipmapPrograms[4]; + SkAutoTUnref 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; diff --git a/src/gpu/gl/GrGLProgram.cpp b/src/gpu/gl/GrGLProgram.cpp index c270858ec3..040c57de3a 100644 --- a/src/gpu/gl/GrGLProgram.cpp +++ b/src/gpu/gl/GrGLProgram.cpp @@ -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(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(access.getTexture())); + } +} diff --git a/src/gpu/gl/GrGLProgram.h b/src/gpu/gl/GrGLProgram.h index 7487a1e900..9f2b2e9b74 100644 --- a/src/gpu/gl/GrGLProgram.h +++ b/src/gpu/gl/GrGLProgram.h @@ -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; diff --git a/tests/SRGBMipMapTest.cpp b/tests/SRGBMipMapTest.cpp new file mode 100644 index 0000000000..f9ee037e7b --- /dev/null +++ b/tests/SRGBMipMapTest.cpp @@ -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 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 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 l32Texture(texProvider->createTexture(l32Desc, SkBudgeted::kNo)); + SkAutoTUnref s32Texture(texProvider->createTexture(s32Desc, SkBudgeted::kNo)); + + SkSurfaceProps l32Props(SkSurfaceProps::kLegacyFontHost_InitType); + SkSurfaceProps s32Props(SkSurfaceProps::kGammaCorrect_Flag, + SkSurfaceProps::kLegacyFontHost_InitType); + + sk_sp l32DrawContext( + context->drawContext(sk_ref_sp(l32Texture->asRenderTarget()), &l32Props)); + sk_sp 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 -- 2.34.1