/* * 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 "src/gpu/ganesh/SurfaceContext.h" #include "include/core/SkColorSpace.h" #include "include/gpu/GrDirectContext.h" #include "include/gpu/GrRecordingContext.h" #include "src/core/SkAutoPixmapStorage.h" #include "src/core/SkMipmap.h" #include "src/core/SkYUVMath.h" #include "src/gpu/ganesh/GrClientMappedBufferManager.h" #include "src/gpu/ganesh/GrColorSpaceXform.h" #include "src/gpu/ganesh/GrDataUtils.h" #include "src/gpu/ganesh/GrDirectContextPriv.h" #include "src/gpu/ganesh/GrDrawingManager.h" #include "src/gpu/ganesh/GrGpu.h" #include "src/gpu/ganesh/GrImageInfo.h" #include "src/gpu/ganesh/GrProxyProvider.h" #include "src/gpu/ganesh/GrRecordingContextPriv.h" #include "src/gpu/ganesh/GrResourceProvider.h" #include "src/gpu/ganesh/GrTracing.h" #include "src/gpu/ganesh/SkGr.h" #include "src/gpu/ganesh/SurfaceFillContext.h" #include "src/gpu/ganesh/effects/GrBicubicEffect.h" #include "src/gpu/ganesh/effects/GrTextureEffect.h" #include "src/gpu/ganesh/geometry/GrRect.h" #include using namespace skia_private; #define ASSERT_SINGLE_OWNER SKGPU_ASSERT_SINGLE_OWNER(this->singleOwner()) #define RETURN_FALSE_IF_ABANDONED if (this->fContext->abandoned()) { return false; } #define RETURN_NULLPTR_IF_ABANDONED if (this->fContext->abandoned()) { return nullptr; } namespace skgpu::ganesh { SurfaceContext::SurfaceContext(GrRecordingContext* context, GrSurfaceProxyView readView, const GrColorInfo& info) : fContext(context), fReadView(std::move(readView)), fColorInfo(info) { SkASSERT(!context->abandoned()); } const GrCaps* SurfaceContext::caps() const { return fContext->priv().caps(); } GrDrawingManager* SurfaceContext::drawingManager() { return fContext->priv().drawingManager(); } const GrDrawingManager* SurfaceContext::drawingManager() const { return fContext->priv().drawingManager(); } #ifdef SK_DEBUG skgpu::SingleOwner* SurfaceContext::singleOwner() const { return fContext->priv().singleOwner(); } #endif static bool alpha_types_compatible(SkAlphaType srcAlphaType, SkAlphaType dstAlphaType) { // If both alpha types are kUnknown things make sense. If not, it's too underspecified. return (srcAlphaType == kUnknown_SkAlphaType) == (dstAlphaType == kUnknown_SkAlphaType); } bool SurfaceContext::readPixels(GrDirectContext* dContext, GrPixmap dst, SkIPoint pt) { ASSERT_SINGLE_OWNER RETURN_FALSE_IF_ABANDONED SkDEBUGCODE(this->validate();) GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceContext", "readPixels", fContext); if (!fContext->priv().matches(dContext)) { return false; } if (dst.colorType() == GrColorType::kUnknown) { return false; } if (dst.rowBytes() % dst.info().bpp()) { return false; } dst = dst.clip(this->dimensions(), &pt); if (!dst.hasPixels()) { return false; } if (!alpha_types_compatible(this->colorInfo().alphaType(), dst.alphaType())) { return false; } // We allow unknown alpha types but only if both src and dst are unknown. Otherwise, it's too // weird to reason about what should be expected. sk_sp srcProxy = this->asSurfaceProxyRef(); if (srcProxy->framebufferOnly()) { return false; } // MDB TODO: delay this instantiation until later in the method if (!srcProxy->instantiate(dContext->priv().resourceProvider())) { return false; } GrSurface* srcSurface = srcProxy->peekSurface(); SkColorSpaceXformSteps::Flags flags = SkColorSpaceXformSteps{this->colorInfo(), dst.info()}.flags; bool unpremul = flags.unpremul, needColorConversion = flags.linearize || flags.gamut_transform || flags.encode, premul = flags.premul; const GrCaps* caps = dContext->priv().caps(); bool srcIsCompressed = caps->isFormatCompressed(srcSurface->backendFormat()); // This is the getImageData equivalent to the canvas2D putImageData fast path. We probably don't // care so much about getImageData performance. However, in order to ensure putImageData/ // getImageData in "legacy" mode are round-trippable we use the GPU to do the complementary // unpremul step to writeSurfacePixels's premul step (which is determined empirically in // fContext->vaildaPMUPMConversionExists()). GrBackendFormat defaultRGBAFormat = caps->getDefaultBackendFormat(GrColorType::kRGBA_8888, GrRenderable::kYes); GrColorType srcColorType = this->colorInfo().colorType(); bool canvas2DFastPath = unpremul && !needColorConversion && (GrColorType::kRGBA_8888 == dst.colorType() || GrColorType::kBGRA_8888 == dst.colorType()) && SkToBool(srcProxy->asTextureProxy()) && (srcColorType == GrColorType::kRGBA_8888 || srcColorType == GrColorType::kBGRA_8888) && defaultRGBAFormat.isValid() && dContext->priv().validPMUPMConversionExists(); // Since the validPMUPMConversionExists function actually submits work to the gpu to do its // tests, it is possible that during that call we have abandoned the context. Thus, we do // another abandoned check here to make sure we are still valid. RETURN_FALSE_IF_ABANDONED auto readFlag = caps->surfaceSupportsReadPixels(srcSurface); if (readFlag == GrCaps::SurfaceReadPixelsSupport::kUnsupported) { return false; } if (readFlag == GrCaps::SurfaceReadPixelsSupport::kCopyToTexture2D || canvas2DFastPath) { std::unique_ptr tempCtx; if (this->asTextureProxy()) { GrColorType colorType = (canvas2DFastPath || srcIsCompressed) ? GrColorType::kRGBA_8888 : this->colorInfo().colorType(); SkAlphaType alphaType = canvas2DFastPath ? dst.alphaType() : this->colorInfo().alphaType(); GrImageInfo tempInfo(colorType, alphaType, this->colorInfo().refColorSpace(), dst.dimensions()); auto sfc = dContext->priv().makeSFC( tempInfo, "SurfaceContext_ReadPixels", SkBackingFit::kApprox); if (!sfc) { return false; } std::unique_ptr fp; if (canvas2DFastPath) { fp = dContext->priv().createPMToUPMEffect(GrTextureEffect::Make( this->readSurfaceView(), this->colorInfo().alphaType())); if (dst.colorType() == GrColorType::kBGRA_8888) { fp = GrFragmentProcessor::SwizzleOutput(std::move(fp), skgpu::Swizzle::BGRA()); dst = GrPixmap(dst.info().makeColorType(GrColorType::kRGBA_8888), dst.addr(), dst.rowBytes()); } } else { fp = GrTextureEffect::Make(this->readSurfaceView(), this->colorInfo().alphaType()); } if (!fp) { return false; } sfc->fillRectToRectWithFP(SkIRect::MakePtSize(pt, dst.dimensions()), SkIRect::MakeSize(dst.dimensions()), std::move(fp)); pt = {0, 0}; tempCtx = std::move(sfc); } else { auto restrictions = this->caps()->getDstCopyRestrictions(this->asRenderTargetProxy(), this->colorInfo().colorType()); sk_sp copy; static constexpr auto kFit = SkBackingFit::kExact; static constexpr auto kBudgeted = skgpu::Budgeted::kYes; static constexpr auto kMipMapped = skgpu::Mipmapped::kNo; if (restrictions.fMustCopyWholeSrc) { copy = GrSurfaceProxy::Copy(fContext, std::move(srcProxy), this->origin(), kMipMapped, kFit, kBudgeted, /*label=*/"SurfaceContext_ReadPixelsWithCopyWholeSrc"); } else { auto srcRect = SkIRect::MakePtSize(pt, dst.dimensions()); copy = GrSurfaceProxy::Copy(fContext, std::move(srcProxy), this->origin(), kMipMapped, srcRect, kFit, kBudgeted, /*label=*/"SurfaceContext_ReadPixels", restrictions.fRectsMustMatch); pt = {0, 0}; } if (!copy) { return false; } GrSurfaceProxyView view{std::move(copy), this->origin(), this->readSwizzle()}; tempCtx = dContext->priv().makeSC(std::move(view), this->colorInfo()); SkASSERT(tempCtx); } return tempCtx->readPixels(dContext, dst, pt); } bool flip = this->origin() == kBottomLeft_GrSurfaceOrigin; auto supportedRead = caps->supportedReadPixelsColorType( this->colorInfo().colorType(), srcProxy->backendFormat(), dst.colorType()); bool makeTight = !caps->readPixelsRowBytesSupport() && dst.rowBytes() != dst.info().minRowBytes(); bool convert = unpremul || premul || needColorConversion || flip || makeTight || (dst.colorType() != supportedRead.fColorType); std::unique_ptr tmpPixels; GrPixmap tmp; void* readDst = dst.addr(); size_t readRB = dst.rowBytes(); if (convert) { GrImageInfo tmpInfo(supportedRead.fColorType, this->colorInfo().alphaType(), this->colorInfo().refColorSpace(), dst.dimensions()); size_t tmpRB = tmpInfo.minRowBytes(); size_t size = tmpRB * tmpInfo.height(); // Chrome MSAN bots require the data to be initialized (hence the ()). tmpPixels = std::make_unique(size); tmp = {tmpInfo, tmpPixels.get(), tmpRB}; readDst = tmpPixels.get(); readRB = tmpRB; pt.fY = flip ? srcSurface->height() - pt.fY - dst.height() : pt.fY; } dContext->priv().flushSurface(srcProxy.get()); dContext->submit(); if (!dContext->priv().getGpu()->readPixels(srcSurface, SkIRect::MakePtSize(pt, dst.dimensions()), this->colorInfo().colorType(), supportedRead.fColorType, readDst, readRB)) { return false; } if (tmp.hasPixels()) { return GrConvertPixels(dst, tmp, flip); } return true; } bool SurfaceContext::writePixels(GrDirectContext* dContext, GrCPixmap src, SkIPoint dstPt) { ASSERT_SINGLE_OWNER RETURN_FALSE_IF_ABANDONED SkDEBUGCODE(this->validate();) src = src.clip(this->dimensions(), &dstPt); if (!src.hasPixels()) { return false; } if (!src.info().bpp() || src.rowBytes() % src.info().bpp()) { return false; } return this->internalWritePixels(dContext, &src, 1, dstPt); } bool SurfaceContext::writePixels(GrDirectContext* dContext, const GrCPixmap src[], int numLevels) { ASSERT_SINGLE_OWNER RETURN_FALSE_IF_ABANDONED SkDEBUGCODE(this->validate();) SkASSERT(dContext); SkASSERT(numLevels >= 1); SkASSERT(src); if (numLevels == 1) { if (src->dimensions() != this->dimensions()) { return false; } return this->writePixels(dContext, src[0], {0, 0}); } if (!this->asTextureProxy() || this->asTextureProxy()->proxyMipmapped() == skgpu::Mipmapped::kNo) { return false; } SkISize dims = this->dimensions(); if (numLevels != SkMipmap::ComputeLevelCount(dims) + 1) { return false; } for (int i = 0; i < numLevels; ++i) { if (src[i].colorInfo() != src[0].colorInfo()) { return false; } if (dims != src[i].dimensions()) { return false; } if (!src[i].info().bpp() || src[i].rowBytes() % src[i].info().bpp()) { return false; } dims = {std::max(1, dims.width()/2), std::max(1, dims.height()/2)}; } return this->internalWritePixels(dContext, src, numLevels, {0, 0}); } bool SurfaceContext::internalWritePixels(GrDirectContext* dContext, const GrCPixmap src[], int numLevels, SkIPoint pt) { GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceContext", "internalWritePixels", fContext); SkASSERT(numLevels >= 1); SkASSERT(src); // We can either write to a subset or write MIP levels, but not both. SkASSERT((src[0].dimensions() == this->dimensions() && pt.isZero()) || numLevels == 1); SkASSERT(numLevels == 1 || (this->asTextureProxy() && this->asTextureProxy()->mipmapped() == skgpu::Mipmapped::kYes)); // Our public caller should have clipped to the bounds of the surface already. SkASSERT(SkIRect::MakeSize(this->dimensions()).contains( SkIRect::MakePtSize(pt, src[0].dimensions()))); if (!dContext) { return false; } if (this->asSurfaceProxy()->readOnly()) { return false; } if (src[0].colorType() == GrColorType::kUnknown) { return false; } if (!alpha_types_compatible(src[0].alphaType(), this->colorInfo().alphaType())) { return false; } GrSurfaceProxy* dstProxy = this->asSurfaceProxy(); if (dstProxy->framebufferOnly()) { return false; } if (!dstProxy->instantiate(dContext->priv().resourceProvider())) { return false; } GrSurface* dstSurface = dstProxy->peekSurface(); SkColorSpaceXformSteps::Flags flags = SkColorSpaceXformSteps{src[0].colorInfo(), this->colorInfo()}.flags; bool unpremul = flags.unpremul, needColorConversion = flags.linearize || flags.gamut_transform || flags.encode, premul = flags.premul; const GrCaps* caps = dContext->priv().caps(); auto rgbaDefaultFormat = caps->getDefaultBackendFormat(GrColorType::kRGBA_8888, GrRenderable::kNo); GrColorType dstColorType = this->colorInfo().colorType(); // For canvas2D putImageData performance we have a special code path for unpremul RGBA_8888 srcs // that are premultiplied on the GPU. This is kept as narrow as possible for now. bool canvas2DFastPath = !caps->avoidWritePixelsFastPath() && premul && !needColorConversion && (src[0].colorType() == GrColorType::kRGBA_8888 || src[0].colorType() == GrColorType::kBGRA_8888) && this->asFillContext() && (dstColorType == GrColorType::kRGBA_8888 || dstColorType == GrColorType::kBGRA_8888) && rgbaDefaultFormat.isValid() && dContext->priv().validPMUPMConversionExists(); // Since the validPMUPMConversionExists function actually submits work to the gpu to do its // tests, it is possible that during that call we have abanoned the context. Thus we do an // abanoned check here to make sure we are still valid. RETURN_FALSE_IF_ABANDONED // Drawing code path doesn't support writing to levels and doesn't support inserting layout // transitions. if ((!caps->surfaceSupportsWritePixels(dstSurface) || canvas2DFastPath) && numLevels == 1) { GrColorInfo tempColorInfo; GrBackendFormat format; skgpu::Swizzle tempReadSwizzle; if (canvas2DFastPath) { tempColorInfo = {GrColorType::kRGBA_8888, kUnpremul_SkAlphaType, this->colorInfo().refColorSpace()}; format = rgbaDefaultFormat; } else { tempColorInfo = this->colorInfo(); format = dstProxy->backendFormat().makeTexture2D(); if (!format.isValid()) { return false; } tempReadSwizzle = this->readSwizzle(); } // It is more efficient for us to write pixels into a top left origin so we prefer that. // However, if the final proxy isn't a render target then we must use a copy to move the // data into it which requires the origins to match. If the final proxy is a render target // we can use a draw instead which doesn't have this origin restriction. Thus for render // targets we will use top left and otherwise we will make the origins match. GrSurfaceOrigin tempOrigin = this->asFillContext() ? kTopLeft_GrSurfaceOrigin : this->origin(); auto tempProxy = dContext->priv().proxyProvider()->createProxy( format, src[0].dimensions(), GrRenderable::kNo, 1, skgpu::Mipmapped::kNo, SkBackingFit::kApprox, skgpu::Budgeted::kYes, GrProtected::kNo, /*label=*/"SurfaceContext_InternalWritePixels"); if (!tempProxy) { return false; } GrSurfaceProxyView tempView(tempProxy, tempOrigin, tempReadSwizzle); SurfaceContext tempCtx(dContext, tempView, tempColorInfo); // In the fast path we always write the srcData to the temp context as though it were RGBA. // When the data is really BGRA the write will cause the R and B channels to be swapped in // the intermediate surface which gets corrected by a swizzle effect when drawing to the // dst. GrCPixmap origSrcBase = src[0]; GrCPixmap srcBase = origSrcBase; if (canvas2DFastPath) { srcBase = GrCPixmap(origSrcBase.info().makeColorType(GrColorType::kRGBA_8888), origSrcBase.addr(), origSrcBase.rowBytes()); } if (!tempCtx.writePixels(dContext, srcBase, {0, 0})) { return false; } if (this->asFillContext()) { std::unique_ptr fp; if (canvas2DFastPath) { fp = dContext->priv().createUPMToPMEffect( GrTextureEffect::Make(std::move(tempView), tempColorInfo.alphaType())); // Important: check the original src color type here! if (origSrcBase.colorType() == GrColorType::kBGRA_8888) { fp = GrFragmentProcessor::SwizzleOutput(std::move(fp), skgpu::Swizzle::BGRA()); } } else { fp = GrTextureEffect::Make(std::move(tempView), tempColorInfo.alphaType()); } if (!fp) { return false; } this->asFillContext()->fillRectToRectWithFP( SkIRect::MakeSize(srcBase.dimensions()), SkIRect::MakePtSize(pt, srcBase.dimensions()), std::move(fp)); } else { SkIRect srcRect = SkIRect::MakeSize(srcBase.dimensions()); SkIPoint dstPoint = SkIPoint::Make(pt.fX, pt.fY); if (!this->copy(std::move(tempProxy), srcRect, dstPoint)) { return false; } } return true; } GrColorType srcColorType = src[0].colorType(); auto [allowedColorType, _] = caps->supportedWritePixelsColorType(this->colorInfo().colorType(), dstProxy->backendFormat(), srcColorType); bool flip = this->origin() == kBottomLeft_GrSurfaceOrigin; bool convertAll = premul || unpremul || needColorConversion || flip || (srcColorType != allowedColorType); bool mustBeTight = !caps->writePixelsRowBytesSupport(); size_t tmpSize = 0; if (mustBeTight || convertAll) { for (int i = 0; i < numLevels; ++i) { if (convertAll || (mustBeTight && src[i].rowBytes() != src[i].info().minRowBytes())) { tmpSize += src[i].info().makeColorType(allowedColorType).minRowBytes()* src[i].height(); } } } auto tmpData = tmpSize ? SkData::MakeUninitialized(tmpSize) : nullptr; void* tmp = tmpSize ? tmpData->writable_data() : nullptr; AutoSTArray<15, GrMipLevel> srcLevels(numLevels); bool ownAllStorage = true; for (int i = 0; i < numLevels; ++i) { if (convertAll || (mustBeTight && src[i].rowBytes() != src[i].info().minRowBytes())) { GrImageInfo tmpInfo(allowedColorType, this->colorInfo().alphaType(), this->colorInfo().refColorSpace(), src[i].dimensions()); auto tmpRB = tmpInfo.minRowBytes(); GrPixmap tmpPM(tmpInfo, tmp, tmpRB); SkAssertResult(GrConvertPixels(tmpPM, src[i], flip)); srcLevels[i] = {tmpPM.addr(), tmpPM.rowBytes(), tmpData}; tmp = SkTAddOffset(tmp, tmpRB*tmpPM.height()); } else { srcLevels[i] = {src[i].addr(), src[i].rowBytes(), src[i].pixelStorage()}; ownAllStorage &= src[i].ownsPixels(); } } pt.fY = flip ? dstSurface->height() - pt.fY - src[0].height() : pt.fY; if (!dContext->priv().drawingManager()->newWritePixelsTask( sk_ref_sp(dstProxy), SkIRect::MakePtSize(pt, src[0].dimensions()), allowedColorType, this->colorInfo().colorType(), srcLevels.begin(), numLevels)) { return false; } if (numLevels > 1) { dstProxy->asTextureProxy()->markMipmapsClean(); } if (!ownAllStorage) { // If any pixmap doesn't own its pixels then we must flush so that the pixels are pushed to // the GPU before we return. dContext->priv().flushSurface(dstProxy); } return true; } void SurfaceContext::asyncRescaleAndReadPixels(GrDirectContext* dContext, const SkImageInfo& info, const SkIRect& srcRect, RescaleGamma rescaleGamma, RescaleMode rescaleMode, ReadPixelsCallback callback, ReadPixelsContext callbackContext) { if (!dContext) { callback(callbackContext, nullptr); return; } auto rt = this->asRenderTargetProxy(); if (rt && rt->wrapsVkSecondaryCB()) { callback(callbackContext, nullptr); return; } if (rt && rt->framebufferOnly()) { callback(callbackContext, nullptr); return; } auto dstCT = SkColorTypeToGrColorType(info.colorType()); if (dstCT == GrColorType::kUnknown) { callback(callbackContext, nullptr); return; } bool needsRescale = srcRect.size() != info.dimensions() || this->origin() == kBottomLeft_GrSurfaceOrigin || this->colorInfo().alphaType() != info.alphaType() || !SkColorSpace::Equals(this->colorInfo().colorSpace(), info.colorSpace()); auto surfaceBackendFormat = this->asSurfaceProxy()->backendFormat(); auto readInfo = this->caps()->supportedReadPixelsColorType(this->colorInfo().colorType(), surfaceBackendFormat, dstCT); // Fail if we can't read from the source surface's color type. if (readInfo.fColorType == GrColorType::kUnknown) { callback(callbackContext, nullptr); return; } // Fail if read color type does not have all of dstCT's color channels and those missing color // channels are in the src. uint32_t dstChannels = GrColorTypeChannelFlags(dstCT); uint32_t legalReadChannels = GrColorTypeChannelFlags(readInfo.fColorType); uint32_t srcChannels = GrColorTypeChannelFlags(this->colorInfo().colorType()); if ((~legalReadChannels & dstChannels) & srcChannels) { callback(callbackContext, nullptr); return; } std::unique_ptr tempFC; int x = srcRect.fLeft; int y = srcRect.fTop; if (needsRescale) { auto tempInfo = GrImageInfo(info).makeColorType(this->colorInfo().colorType()); tempFC = this->rescale(tempInfo, kTopLeft_GrSurfaceOrigin, srcRect, rescaleGamma, rescaleMode); if (!tempFC) { callback(callbackContext, nullptr); return; } SkASSERT(SkColorSpace::Equals(tempFC->colorInfo().colorSpace(), info.colorSpace())); SkASSERT(tempFC->origin() == kTopLeft_GrSurfaceOrigin); x = y = 0; } auto srcCtx = tempFC ? tempFC.get() : this; return srcCtx->asyncReadPixels(dContext, SkIRect::MakePtSize({x, y}, info.dimensions()), info.colorType(), callback, callbackContext); } void SurfaceContext::asyncReadPixels(GrDirectContext* dContext, const SkIRect& rect, SkColorType colorType, ReadPixelsCallback callback, ReadPixelsContext callbackContext) { using AsyncReadResult = skgpu::TAsyncReadResult; SkASSERT(rect.fLeft >= 0 && rect.fRight <= this->width()); SkASSERT(rect.fTop >= 0 && rect.fBottom <= this->height()); if (!dContext || this->asSurfaceProxy()->isProtected() == GrProtected::kYes) { callback(callbackContext, nullptr); return; } auto mappedBufferManager = dContext->priv().clientMappedBufferManager(); auto transferResult = this->transferPixels(SkColorTypeToGrColorType(colorType), rect); if (!transferResult.fTransferBuffer) { auto ii = SkImageInfo::Make(rect.size(), colorType, this->colorInfo().alphaType(), this->colorInfo().refColorSpace()); static const GrDirectContext::DirectContextID kInvalid; auto result = std::make_unique(kInvalid); GrPixmap pm = GrPixmap::Allocate(ii); result->addCpuPlane(pm.pixelStorage(), pm.rowBytes()); SkIPoint pt{rect.fLeft, rect.fTop}; if (!this->readPixels(dContext, pm, pt)) { callback(callbackContext, nullptr); return; } callback(callbackContext, std::move(result)); return; } struct FinishContext { ReadPixelsCallback* fClientCallback; ReadPixelsContext fClientContext; SkISize fSize; GrClientMappedBufferManager* fMappedBufferManager; PixelTransferResult fTransferResult; }; // Assumption is that the caller would like to flush. We could take a parameter or require an // explicit flush from the caller. We'd have to have a way to defer attaching the finish // callback to GrGpu until after the next flush that flushes our op list, though. auto* finishContext = new FinishContext{callback, callbackContext, rect.size(), mappedBufferManager, std::move(transferResult)}; auto finishCallback = [](GrGpuFinishedContext c) { const auto* context = reinterpret_cast(c); auto manager = context->fMappedBufferManager; auto result = std::make_unique(manager->ownerID()); if (!result->addTransferResult(context->fTransferResult, context->fSize, context->fTransferResult.fRowBytes, manager)) { result.reset(); } (*context->fClientCallback)(context->fClientContext, std::move(result)); delete context; }; GrFlushInfo flushInfo; flushInfo.fFinishedContext = finishContext; flushInfo.fFinishedProc = finishCallback; dContext->priv().flushSurface( this->asSurfaceProxy(), SkSurfaces::BackendSurfaceAccess::kNoAccess, flushInfo); } void SurfaceContext::asyncRescaleAndReadPixelsYUV420(GrDirectContext* dContext, SkYUVColorSpace yuvColorSpace, bool readAlpha, sk_sp dstColorSpace, const SkIRect& srcRect, SkISize dstSize, RescaleGamma rescaleGamma, RescaleMode rescaleMode, ReadPixelsCallback callback, ReadPixelsContext callbackContext) { using AsyncReadResult = skgpu::TAsyncReadResult; SkASSERT(srcRect.fLeft >= 0 && srcRect.fRight <= this->width()); SkASSERT(srcRect.fTop >= 0 && srcRect.fBottom <= this->height()); SkASSERT(!dstSize.isZero()); SkASSERT((dstSize.width() % 2 == 0) && (dstSize.height() % 2 == 0)); if (!dContext) { callback(callbackContext, nullptr); return; } auto rt = this->asRenderTargetProxy(); if (rt && rt->wrapsVkSecondaryCB()) { callback(callbackContext, nullptr); return; } if (rt && rt->framebufferOnly()) { callback(callbackContext, nullptr); return; } if (this->asSurfaceProxy()->isProtected() == GrProtected::kYes) { callback(callbackContext, nullptr); return; } int x = srcRect.fLeft; int y = srcRect.fTop; bool needsRescale = srcRect.size() != dstSize || !SkColorSpace::Equals(this->colorInfo().colorSpace(), dstColorSpace.get()); GrSurfaceProxyView srcView = this->readSurfaceView(); if (needsRescale) { auto info = SkImageInfo::Make(dstSize, kRGBA_8888_SkColorType, this->colorInfo().alphaType(), dstColorSpace); // TODO: Incorporate the YUV conversion into last pass of rescaling. auto tempFC = this->rescale(info, kTopLeft_GrSurfaceOrigin, srcRect, rescaleGamma, rescaleMode); if (!tempFC) { callback(callbackContext, nullptr); return; } SkASSERT(SkColorSpace::Equals(tempFC->colorInfo().colorSpace(), info.colorSpace())); SkASSERT(tempFC->origin() == kTopLeft_GrSurfaceOrigin); x = y = 0; srcView = tempFC->readSurfaceView(); } else if (!srcView.asTextureProxy()) { srcView = GrSurfaceProxyView::Copy( fContext, std::move(srcView), skgpu::Mipmapped::kNo, srcRect, SkBackingFit::kApprox, skgpu::Budgeted::kYes, /*label=*/"SurfaceContext_AsyncRescaleAndReadPixelsYUV420"); if (!srcView) { // If we can't get a texture copy of the contents then give up. callback(callbackContext, nullptr); return; } SkASSERT(srcView.asTextureProxy()); x = y = 0; } auto yaInfo = SkImageInfo::MakeA8(dstSize); auto yFC = dContext->priv().makeSFCWithFallback(yaInfo, SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, skgpu::Protected::kNo); std::unique_ptr aFC; if (readAlpha) { aFC = dContext->priv().makeSFCWithFallback(yaInfo, SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, skgpu::Protected::kNo); } auto uvInfo = yaInfo.makeWH(yaInfo.width()/2, yaInfo.height()/2); auto uFC = dContext->priv().makeSFCWithFallback(uvInfo, SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, skgpu::Protected::kNo); auto vFC = dContext->priv().makeSFCWithFallback(uvInfo, SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, skgpu::Protected::kNo); if (!yFC || !uFC || !vFC || (readAlpha && !aFC)) { callback(callbackContext, nullptr); return; } float baseM[20]; SkColorMatrix_RGB2YUV(yuvColorSpace, baseM); // TODO: Use one transfer buffer for all three planes to reduce map/unmap cost? auto texMatrix = SkMatrix::Translate(x, y); auto [readCT, offsetAlignment] = this->caps()->supportedReadPixelsColorType(yFC->colorInfo().colorType(), yFC->asSurfaceProxy()->backendFormat(), GrColorType::kAlpha_8); if (readCT == GrColorType::kUnknown) { callback(callbackContext, nullptr); return; } bool doSynchronousRead = !this->caps()->transferFromSurfaceToBufferSupport() || !offsetAlignment; PixelTransferResult yTransfer, aTransfer, uTransfer, vTransfer; // This matrix generates (r,g,b,a) = (0, 0, 0, y) float yM[20]; std::fill_n(yM, 15, 0.f); std::copy_n(baseM + 0, 5, yM + 15); auto yFP = GrTextureEffect::Make(srcView, this->colorInfo().alphaType(), texMatrix); yFP = GrFragmentProcessor::ColorMatrix(std::move(yFP), yM, /*unpremulInput=*/false, /*clampRGBOutput=*/true, /*premulOutput=*/false); yFC->fillWithFP(std::move(yFP)); if (!doSynchronousRead) { yTransfer = yFC->transferPixels(GrColorType::kAlpha_8, SkIRect::MakeSize(yFC->dimensions())); if (!yTransfer.fTransferBuffer) { callback(callbackContext, nullptr); return; } } if (readAlpha) { auto aFP = GrTextureEffect::Make(srcView, this->colorInfo().alphaType(), texMatrix); SkASSERT(baseM[15] == 0 && baseM[16] == 0 && baseM[17] == 0 && baseM[18] == 1 && baseM[19] == 0); aFC->fillWithFP(std::move(aFP)); if (!doSynchronousRead) { aTransfer = aFC->transferPixels(GrColorType::kAlpha_8, SkIRect::MakeSize(aFC->dimensions())); if (!aTransfer.fTransferBuffer) { callback(callbackContext, nullptr); return; } } } texMatrix.preScale(2.f, 2.f); // This matrix generates (r,g,b,a) = (0, 0, 0, u) float uM[20]; std::fill_n(uM, 15, 0.f); std::copy_n(baseM + 5, 5, uM + 15); auto uFP = GrTextureEffect::Make(srcView, this->colorInfo().alphaType(), texMatrix, GrSamplerState::Filter::kLinear); uFP = GrFragmentProcessor::ColorMatrix(std::move(uFP), uM, /*unpremulInput=*/false, /*clampRGBOutput=*/true, /*premulOutput=*/false); uFC->fillWithFP(std::move(uFP)); if (!doSynchronousRead) { uTransfer = uFC->transferPixels(GrColorType::kAlpha_8, SkIRect::MakeSize(uFC->dimensions())); if (!uTransfer.fTransferBuffer) { callback(callbackContext, nullptr); return; } } // This matrix generates (r,g,b,a) = (0, 0, 0, v) float vM[20]; std::fill_n(vM, 15, 0.f); std::copy_n(baseM + 10, 5, vM + 15); auto vFP = GrTextureEffect::Make(std::move(srcView), this->colorInfo().alphaType(), texMatrix, GrSamplerState::Filter::kLinear); vFP = GrFragmentProcessor::ColorMatrix(std::move(vFP), vM, /*unpremulInput=*/false, /*clampRGBOutput=*/true, /*premulOutput=*/false); vFC->fillWithFP(std::move(vFP)); if (!doSynchronousRead) { vTransfer = vFC->transferPixels(GrColorType::kAlpha_8, SkIRect::MakeSize(vFC->dimensions())); if (!vTransfer.fTransferBuffer) { callback(callbackContext, nullptr); return; } } if (doSynchronousRead) { GrPixmap yPmp = GrPixmap::Allocate(yaInfo); GrPixmap uPmp = GrPixmap::Allocate(uvInfo); GrPixmap vPmp = GrPixmap::Allocate(uvInfo); GrPixmap aPmp; if (readAlpha) { aPmp = GrPixmap::Allocate(yaInfo); } if (!yFC->readPixels(dContext, yPmp, {0, 0}) || !uFC->readPixels(dContext, uPmp, {0, 0}) || !vFC->readPixels(dContext, vPmp, {0, 0}) || (readAlpha && !aFC->readPixels(dContext, aPmp, {0, 0}))) { callback(callbackContext, nullptr); return; } auto result = std::make_unique(dContext->directContextID()); result->addCpuPlane(yPmp.pixelStorage(), yPmp.rowBytes()); result->addCpuPlane(uPmp.pixelStorage(), uPmp.rowBytes()); result->addCpuPlane(vPmp.pixelStorage(), vPmp.rowBytes()); if (readAlpha) { result->addCpuPlane(aPmp.pixelStorage(), aPmp.rowBytes()); } callback(callbackContext, std::move(result)); return; } struct FinishContext { ReadPixelsCallback* fClientCallback; ReadPixelsContext fClientContext; GrClientMappedBufferManager* fMappedBufferManager; SkISize fSize; PixelTransferResult fYTransfer; PixelTransferResult fUTransfer; PixelTransferResult fVTransfer; PixelTransferResult fATransfer; }; // Assumption is that the caller would like to flush. We could take a parameter or require an // explicit flush from the caller. We'd have to have a way to defer attaching the finish // callback to GrGpu until after the next flush that flushes our op list, though. auto* finishContext = new FinishContext{callback, callbackContext, dContext->priv().clientMappedBufferManager(), dstSize, std::move(yTransfer), std::move(uTransfer), std::move(vTransfer), std::move(aTransfer)}; auto finishCallback = [](GrGpuFinishedContext c) { const auto* context = reinterpret_cast(c); auto manager = context->fMappedBufferManager; auto result = std::make_unique(manager->ownerID()); if (!result->addTransferResult(context->fYTransfer, context->fSize, context->fYTransfer.fRowBytes, manager)) { (*context->fClientCallback)(context->fClientContext, nullptr); delete context; return; } SkISize uvSize = {context->fSize.width() / 2, context->fSize.height() / 2}; if (!result->addTransferResult(context->fUTransfer, uvSize, context->fUTransfer.fRowBytes, manager)) { (*context->fClientCallback)(context->fClientContext, nullptr); delete context; return; } if (!result->addTransferResult(context->fVTransfer, uvSize, context->fVTransfer.fRowBytes, manager)) { (*context->fClientCallback)(context->fClientContext, nullptr); delete context; return; } if (context->fATransfer.fTransferBuffer && !result->addTransferResult(context->fATransfer, context->fSize, context->fATransfer.fRowBytes, manager)) { (*context->fClientCallback)(context->fClientContext, nullptr); delete context; return; } (*context->fClientCallback)(context->fClientContext, std::move(result)); delete context; }; GrFlushInfo flushInfo; flushInfo.fFinishedContext = finishContext; flushInfo.fFinishedProc = finishCallback; dContext->priv().flushSurface( this->asSurfaceProxy(), SkSurfaces::BackendSurfaceAccess::kNoAccess, flushInfo); } sk_sp SurfaceContext::copy(sk_sp src, SkIRect srcRect, SkIPoint dstPoint) { if (!GrClipSrcRectAndDstPoint(this->dimensions(), &dstPoint, src->dimensions(), &srcRect)) { return nullptr; } SkIRect dstRect = SkIRect::MakePtSize(dstPoint, srcRect.size()); return this->copyScaled(src, srcRect, dstRect, GrSamplerState::Filter::kNearest); } sk_sp SurfaceContext::copyScaled(sk_sp src, SkIRect srcRect, SkIRect dstRect, GrSamplerState::Filter filter) { ASSERT_SINGLE_OWNER RETURN_NULLPTR_IF_ABANDONED SkDEBUGCODE(this->validate();) GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceContext", "copyScaled", fContext); const GrCaps* caps = fContext->priv().caps(); if (this->asSurfaceProxy()->framebufferOnly()) { return nullptr; } // canCopySurface() verifies that src and dst rects are contained in their surfaces. if (!caps->canCopySurface(this->asSurfaceProxy(), dstRect, src.get(), srcRect)) { return nullptr; } if (filter == GrSamplerState::Filter::kLinear && !src->isFunctionallyExact()) { // If we're linear filtering an image that is approx-sized, there are cases where the filter // could sample outside the logical dimensions. Specifically if we're upscaling along an // axis where we are copying up to the logical dimension, but that dimension is less than // the actual backing store dimension, the linear filter will access one texel beyond the // logical size, potentially incorporating undefined values. // NOTE: Identity scales that sample along the logical boundary of an approxi-fit texture // can still technically access a row or column of undefined data (albeit with a weight that // *should* be zero). We also disallow copying with linear filtering in that scenario, // just in case. #if defined(SK_USE_SAFE_INSET_FOR_TEXTURE_SAMPLING) const bool upscalingXAtApproxEdge = dstRect.width() >= srcRect.width() && srcRect.fRight == src->width() && srcRect.fRight < src->backingStoreDimensions().width(); const bool upscalingYAtApproxEdge = dstRect.height() >= srcRect.height() && srcRect.fBottom == src->height() && srcRect.fBottom < src->backingStoreDimensions().height(); #else // In this mode we allow non-scaling copies through even if the linear filtering would // access the adjacent undefined row or column. const bool upscalingXAtApproxEdge = dstRect.width() > srcRect.width() && srcRect.fRight == src->width() && srcRect.fRight < src->backingStoreDimensions().width(); const bool upscalingYAtApproxEdge = dstRect.height() > srcRect.height() && srcRect.fBottom == src->height() && srcRect.fBottom < src->backingStoreDimensions().height(); #endif if (upscalingXAtApproxEdge || upscalingYAtApproxEdge) { return nullptr; } // NOTE: Any upscaling with the linear filter will include content that's 1px outside the // src rect, but as long as that's still within the logical dimensions we assume it's okay. } SkASSERT(src->backendFormat().textureType() != GrTextureType::kExternal); SkASSERT(src->backendFormat() == this->asSurfaceProxy()->backendFormat()); return this->drawingManager()->newCopyRenderTask(this->asSurfaceProxyRef(), dstRect, std::move(src), srcRect, filter, this->origin()); } std::unique_ptr SurfaceContext::rescale(const GrImageInfo& info, GrSurfaceOrigin origin, SkIRect srcRect, RescaleGamma rescaleGamma, RescaleMode rescaleMode) { auto sfc = fContext->priv().makeSFCWithFallback(info, SkBackingFit::kExact, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, this->asSurfaceProxy()->isProtected(), origin); if (!sfc || !this->rescaleInto(sfc.get(), SkIRect::MakeSize(sfc->dimensions()), srcRect, rescaleGamma, rescaleMode)) { return nullptr; } return sfc; } bool SurfaceContext::rescaleInto(SurfaceFillContext* dst, SkIRect dstRect, SkIRect srcRect, RescaleGamma rescaleGamma, RescaleMode rescaleMode) { SkASSERT(dst); if (!SkIRect::MakeSize(dst->dimensions()).contains((dstRect))) { return false; } auto rtProxy = this->asRenderTargetProxy(); if (rtProxy && rtProxy->wrapsVkSecondaryCB()) { return false; } if (this->asSurfaceProxy()->framebufferOnly()) { return false; } GrSurfaceProxyView texView = this->readSurfaceView(); // If we perform scaling as draws, texView must be texturable; if it's not already, we have to // make a copy. However, if the scaling can use copyScaled(), we can avoid this copy. auto ensureTexturable = [this](GrSurfaceProxyView texView, SkIRect srcRect) { if (!texView.asTextureProxy()) { // TODO: If copying supported specifying a renderable copy then we could return the copy // when there are no other conversions. texView = GrSurfaceProxyView::Copy(fContext, std::move(texView), skgpu::Mipmapped::kNo, srcRect, SkBackingFit::kApprox, skgpu::Budgeted::kNo, "SurfaceContext_RescaleInto"); if (texView) { SkASSERT(texView.asTextureProxy()); srcRect = SkIRect::MakeSize(srcRect.size()); } } return std::make_pair(std::move(texView), srcRect); }; SkISize finalSize = dstRect.size(); if (finalSize == srcRect.size()) { rescaleGamma = RescaleGamma::kSrc; rescaleMode = RescaleMode::kNearest; } // Within a rescaling pass A is the input (if not null) and B is the output. At the end of the // pass B is moved to A. If 'this' is the input on the first pass then tempA is null. std::unique_ptr tempA; std::unique_ptr tempB; // Assume we should ignore the rescale linear request if the surface has no color space since // it's unclear how we'd linearize from an unknown color space. if (rescaleGamma == RescaleGamma::kLinear && this->colorInfo().colorSpace() && !this->colorInfo().colorSpace()->gammaIsLinear()) { // Colorspace transformations are always handled by drawing so we need to be texturable std::tie(texView, srcRect) = ensureTexturable(texView, srcRect); if (!texView) { return false; } auto cs = this->colorInfo().colorSpace()->makeLinearGamma(); // We'll fall back to kRGBA_8888 if half float not supported. GrImageInfo ii(GrColorType::kRGBA_F16, dst->colorInfo().alphaType(), std::move(cs), srcRect.size()); auto linearRTC = fContext->priv().makeSFCWithFallback(std::move(ii), SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, texView.proxy()->isProtected(), dst->origin()); if (!linearRTC) { return false; } auto fp = GrTextureEffect::Make(std::move(texView), this->colorInfo().alphaType(), SkMatrix::Translate(srcRect.topLeft()), GrSamplerState::Filter::kNearest, GrSamplerState::MipmapMode::kNone); fp = GrColorSpaceXformEffect::Make(std::move(fp), this->colorInfo(), linearRTC->colorInfo()); linearRTC->fillWithFP(std::move(fp)); texView = linearRTC->readSurfaceView(); SkASSERT(texView.asTextureProxy()); tempA = std::move(linearRTC); srcRect = SkIRect::MakeSize(srcRect.size()); } do { SkISize nextDims = finalSize; if (rescaleMode != RescaleMode::kNearest && rescaleMode != RescaleMode::kLinear) { if (srcRect.width() > finalSize.width()) { nextDims.fWidth = std::max((srcRect.width() + 1)/2, finalSize.width()); } else if (srcRect.width() < finalSize.width()) { nextDims.fWidth = std::min(srcRect.width()*2, finalSize.width()); } if (srcRect.height() > finalSize.height()) { nextDims.fHeight = std::max((srcRect.height() + 1)/2, finalSize.height()); } else if (srcRect.height() < finalSize.height()) { nextDims.fHeight = std::min(srcRect.height()*2, finalSize.height()); } } auto input = tempA ? tempA.get() : this; sk_sp xform; SurfaceFillContext* stepDst; SkIRect stepDstRect; if (nextDims == finalSize) { stepDst = dst; stepDstRect = dstRect; xform = GrColorSpaceXform::Make(input->colorInfo(), dst->colorInfo()); } else { GrImageInfo nextInfo(input->colorInfo(), nextDims); tempB = fContext->priv().makeSFCWithFallback(nextInfo, SkBackingFit::kApprox, /* sampleCount= */ 1, skgpu::Mipmapped::kNo, texView.proxy()->isProtected()); if (!tempB) { return false; } stepDst = tempB.get(); stepDstRect = SkIRect::MakeSize(tempB->dimensions()); } std::unique_ptr fp; if (rescaleMode == RescaleMode::kRepeatedCubic) { // Cubic sampling is always handled by drawing with a shader, so we must be texturable std::tie(texView, srcRect) = ensureTexturable(texView, srcRect); if (!texView) { return false; } auto dir = GrBicubicEffect::Direction::kXY; if (nextDims.width() == srcRect.width()) { dir = GrBicubicEffect::Direction::kY; } else if (nextDims.height() == srcRect.height()) { dir = GrBicubicEffect::Direction::kX; } static constexpr auto kWM = GrSamplerState::WrapMode::kClamp; static constexpr auto kKernel = GrBicubicEffect::gCatmullRom; fp = GrBicubicEffect::MakeSubset(std::move(texView), input->colorInfo().alphaType(), SkMatrix::I(), kWM, kWM, SkRect::Make(srcRect), kKernel, dir, *this->caps()); } else { auto filter = rescaleMode == RescaleMode::kNearest ? GrSamplerState::Filter::kNearest : GrSamplerState::Filter::kLinear; if (xform || texView.origin() != stepDst->origin() || !stepDst->copyScaled(texView.refProxy(), srcRect, stepDstRect, filter)) { // We could not or were unable to successful perform a scaling blit (which can be // much faster if texView isn't already texturable). Scale by drawing instead. std::tie(texView, srcRect) = ensureTexturable(texView, srcRect); if (!texView) { return false; } auto srcRectF = SkRect::Make(srcRect); fp = GrTextureEffect::MakeSubset(std::move(texView), this->colorInfo().alphaType(), SkMatrix::I(), {filter, GrSamplerState::MipmapMode::kNone}, srcRectF, srcRectF, *this->caps()); } } if (xform) { SkASSERT(SkToBool(fp)); // shouldn't have done a copy if there was a color xform fp = GrColorSpaceXformEffect::Make(std::move(fp), std::move(xform)); } if (fp) { // When fp is not null, we scale by drawing; if it is null, presumably the src has // already been copied into stepDst. stepDst->fillRectToRectWithFP(srcRect, stepDstRect, std::move(fp)); } texView = stepDst->readSurfaceView(); tempA = std::move(tempB); srcRect = SkIRect::MakeSize(nextDims); } while (srcRect.size() != finalSize); return true; } SurfaceContext::PixelTransferResult SurfaceContext::transferPixels(GrColorType dstCT, const SkIRect& rect) { SkASSERT(rect.fLeft >= 0 && rect.fRight <= this->width()); SkASSERT(rect.fTop >= 0 && rect.fBottom <= this->height()); auto direct = fContext->asDirectContext(); if (!direct) { return {}; } auto rtProxy = this->asRenderTargetProxy(); if (rtProxy && rtProxy->wrapsVkSecondaryCB()) { return {}; } auto proxy = this->asSurfaceProxy(); auto supportedRead = this->caps()->supportedReadPixelsColorType(this->colorInfo().colorType(), proxy->backendFormat(), dstCT); // Fail if read color type does not have all of dstCT's color channels and those missing color // channels are in the src. uint32_t dstChannels = GrColorTypeChannelFlags(dstCT); uint32_t legalReadChannels = GrColorTypeChannelFlags(supportedRead.fColorType); uint32_t srcChannels = GrColorTypeChannelFlags(this->colorInfo().colorType()); if ((~legalReadChannels & dstChannels) & srcChannels) { return {}; } if (!this->caps()->transferFromSurfaceToBufferSupport() || !supportedRead.fOffsetAlignmentForTransferBuffer) { return {}; } size_t rowBytes = GrColorTypeBytesPerPixel(supportedRead.fColorType) * rect.width(); rowBytes = SkAlignTo(rowBytes, this->caps()->transferBufferRowBytesAlignment()); size_t size = rowBytes * rect.height(); // By using kStream_GrAccessPattern here, we are not able to cache and reuse the buffer for // multiple reads. Switching to kDynamic_GrAccessPattern would allow for this, however doing // so causes a crash in a chromium test. See skbug.com/11297 auto buffer = direct->priv().resourceProvider()->createBuffer( size, GrGpuBufferType::kXferGpuToCpu, GrAccessPattern::kStream_GrAccessPattern, GrResourceProvider::ZeroInit::kNo); if (!buffer) { return {}; } auto srcRect = rect; bool flip = this->origin() == kBottomLeft_GrSurfaceOrigin; if (flip) { srcRect = SkIRect::MakeLTRB(rect.fLeft, this->height() - rect.fBottom, rect.fRight, this->height() - rect.fTop); } this->drawingManager()->newTransferFromRenderTask(this->asSurfaceProxyRef(), srcRect, this->colorInfo().colorType(), supportedRead.fColorType, buffer, 0); PixelTransferResult result; result.fTransferBuffer = std::move(buffer); auto at = this->colorInfo().alphaType(); if (supportedRead.fColorType != dstCT || flip) { int w = rect.width(), h = rect.height(); GrImageInfo srcInfo(supportedRead.fColorType, at, nullptr, w, h); GrImageInfo dstInfo(dstCT, at, nullptr, w, h); result.fRowBytes = dstInfo.minRowBytes(); result.fPixelConverter = [dstInfo, srcInfo, rowBytes]( void* dst, const void* src) { GrConvertPixels( GrPixmap(dstInfo, dst, dstInfo.minRowBytes()), GrCPixmap(srcInfo, src, rowBytes)); }; } else { result.fRowBytes = rowBytes; } return result; } #ifdef SK_DEBUG void SurfaceContext::validate() const { SkASSERT(fReadView.proxy()); fReadView.proxy()->validate(fContext); if (this->colorInfo().colorType() != GrColorType::kUnknown) { SkASSERT(fContext->priv().caps()->areColorTypeAndFormatCompatible( this->colorInfo().colorType(), fReadView.proxy()->backendFormat())); } this->onValidate(); } #endif } // namespace skgpu::ganesh