diff options
-rw-r--r-- | gm/gamma.cpp | 19 | ||||
-rw-r--r-- | include/gpu/GrTexture.h | 3 | ||||
-rw-r--r-- | src/gpu/GrTexture.cpp | 6 | ||||
-rw-r--r-- | src/gpu/GrTexturePriv.h | 8 | ||||
-rw-r--r-- | src/gpu/gl/GrGLGpu.cpp | 464 | ||||
-rw-r--r-- | src/gpu/gl/GrGLGpu.h | 20 | ||||
-rw-r--r-- | src/gpu/gl/GrGLProgram.cpp | 26 | ||||
-rw-r--r-- | src/gpu/gl/GrGLProgram.h | 9 | ||||
-rw-r--r-- | tests/SRGBMipMapTest.cpp | 166 |
9 files changed, 686 insertions, 35 deletions
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<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); 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<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, 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<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; 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<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())); + } +} 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<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 |