/*
 * Copyright 2013 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <set>
#include "include/core/SkSurface.h"
#include "include/gpu/GrContext.h"
#include "include/gpu/GrTexture.h"
#include "src/core/SkAutoPixmapStorage.h"
#include "src/core/SkMipMap.h"
#include "src/gpu/GrClip.h"
#include "src/gpu/GrContextPriv.h"
#include "src/gpu/GrDataUtils.h"
#include "src/gpu/GrGpu.h"
#include "src/gpu/GrProxyProvider.h"
#include "src/gpu/GrRenderTarget.h"
#include "src/gpu/GrResourceProvider.h"
#include "src/gpu/GrTexturePriv.h"
#include "tests/Test.h"
#include "tests/TestUtils.h"

// Tests that GrSurface::asTexture(), GrSurface::asRenderTarget(), and static upcasting of texture
// and render targets to GrSurface all work as expected.
DEF_GPUTEST_FOR_MOCK_CONTEXT(GrSurface, reporter, ctxInfo) {
    GrContext* context = ctxInfo.grContext();
    auto resourceProvider = context->priv().resourceProvider();

    GrSurfaceDesc desc;
    desc.fWidth = 256;
    desc.fHeight = 256;
    desc.fConfig = kRGBA_8888_GrPixelConfig;
    auto format = context->priv().caps()->getDefaultBackendFormat(GrColorType::kRGBA_8888,
                                                                  GrRenderable::kYes);
    sk_sp<GrSurface> texRT1 =
            resourceProvider->createTexture(desc, format, GrRenderable::kYes, 1, GrMipMapped::kNo,
                                            SkBudgeted::kNo, GrProtected::kNo);

    REPORTER_ASSERT(reporter, texRT1.get() == texRT1->asRenderTarget());
    REPORTER_ASSERT(reporter, texRT1.get() == texRT1->asTexture());
    REPORTER_ASSERT(reporter, static_cast<GrSurface*>(texRT1->asRenderTarget()) ==
                    texRT1->asTexture());
    REPORTER_ASSERT(reporter, texRT1->asRenderTarget() ==
                    static_cast<GrSurface*>(texRT1->asTexture()));
    REPORTER_ASSERT(reporter, static_cast<GrSurface*>(texRT1->asRenderTarget()) ==
                    static_cast<GrSurface*>(texRT1->asTexture()));

    sk_sp<GrTexture> tex1 =
            resourceProvider->createTexture(desc, format, GrRenderable::kNo, 1, GrMipMapped::kNo,
                                            SkBudgeted::kNo, GrProtected::kNo);
    REPORTER_ASSERT(reporter, nullptr == tex1->asRenderTarget());
    REPORTER_ASSERT(reporter, tex1.get() == tex1->asTexture());
    REPORTER_ASSERT(reporter, static_cast<GrSurface*>(tex1.get()) == tex1->asTexture());

    GrBackendTexture backendTex = context->createBackendTexture(
        256, 256, kRGBA_8888_SkColorType,
        SkColors::kTransparent, GrMipMapped::kNo, GrRenderable::kNo, GrProtected::kNo);

    sk_sp<GrSurface> texRT2 = resourceProvider->wrapRenderableBackendTexture(
            backendTex, 1, GrColorType::kRGBA_8888, kBorrow_GrWrapOwnership, GrWrapCacheable::kNo);

    REPORTER_ASSERT(reporter, texRT2.get() == texRT2->asRenderTarget());
    REPORTER_ASSERT(reporter, texRT2.get() == texRT2->asTexture());
    REPORTER_ASSERT(reporter, static_cast<GrSurface*>(texRT2->asRenderTarget()) ==
                    texRT2->asTexture());
    REPORTER_ASSERT(reporter, texRT2->asRenderTarget() ==
                    static_cast<GrSurface*>(texRT2->asTexture()));
    REPORTER_ASSERT(reporter, static_cast<GrSurface*>(texRT2->asRenderTarget()) ==
                    static_cast<GrSurface*>(texRT2->asTexture()));

    context->deleteBackendTexture(backendTex);
}

// This test checks that the isFormatTexturable and isFormatRenderable are
// consistent with createTexture's result.
DEF_GPUTEST_FOR_ALL_CONTEXTS(GrSurfaceRenderability, reporter, ctxInfo) {
    GrContext* context = ctxInfo.grContext();
    GrProxyProvider* proxyProvider = context->priv().proxyProvider();
    GrResourceProvider* resourceProvider = context->priv().resourceProvider();
    const GrCaps* caps = context->priv().caps();

    // TODO: Should only need format here but need to determine compression type from format
    // without config.
    auto createTexture = [](int width, int height, GrColorType colorType,
                            const GrBackendFormat& format, GrRenderable renderable,
                            GrResourceProvider* rp) -> sk_sp<GrTexture> {
        GrPixelConfig config = rp->caps()->getConfigFromBackendFormat(format, colorType);
        bool compressed = rp->caps()->isFormatCompressed(format);
        if (compressed) {
            if (renderable == GrRenderable::kYes) {
                return nullptr;
            }
            SkImage::CompressionType type;
            switch (config) {
                case kRGB_ETC1_GrPixelConfig:
                    type = SkImage::kETC1_CompressionType;
                    break;
                default:
                    SK_ABORT("Unexpected config");
            }
            // Only supported compression type right now.
            SkASSERT(config == kRGB_ETC1_GrPixelConfig);
            auto size = GrCompressedDataSize(type, width, height);
            auto data = SkData::MakeUninitialized(size);
            SkColor4f color = {0, 0, 0, 0};
            GrFillInCompressedData(type, width, height, (char*)data->writable_data(), color);
            return rp->createCompressedTexture(width, height, format,
                                               SkImage::kETC1_CompressionType,
                                               SkBudgeted::kNo, data.get());
        } else {
            GrSurfaceDesc desc;
            desc.fWidth = width;
            desc.fHeight = height;
            desc.fConfig = config;
            return rp->createTexture(desc, format, renderable, 1, GrMipMapped::kNo, SkBudgeted::kNo,
                                     GrProtected::kNo);
        }
    };

    static constexpr int kW = 64;
    static constexpr int kH = 64;

    const std::vector<GrCaps::TestFormatColorTypeCombination>& combos =
                                                                    caps->getTestingCombinations();

    for (auto combo : combos) {

        SkASSERT(combo.fColorType != GrColorType::kUnknown);
        SkASSERT(combo.fFormat.isValid());

        // Right now Vulkan has two backend formats that support ABGR_4444 (R4G4B4A4 and B4G4R4A4).
        // Until we can create textures directly from the backend format this yields some
        // ambiguity in what is actually supported and which textures can be created.
        if (ctxInfo.backend() == kVulkan_GrBackend && combo.fColorType == GrColorType::kABGR_4444) {
            continue;
        }

        for (GrSurfaceOrigin origin : { kTopLeft_GrSurfaceOrigin, kBottomLeft_GrSurfaceOrigin }) {
            GrSurfaceDesc desc;
            desc.fWidth = kW;
            desc.fHeight = kH;
            desc.fConfig = caps->getConfigFromBackendFormat(combo.fFormat, combo.fColorType);
            SkASSERT(desc.fConfig != kUnknown_GrPixelConfig);

            // Check if 'isFormatTexturable' agrees with 'createTexture' and that the mipmap
            // support check is working
            {

                bool compressed = caps->isFormatCompressed(combo.fFormat);
                bool isTexturable;
                if (compressed) {
                    isTexturable = caps->isFormatTexturable(combo.fFormat);
                } else {
                    isTexturable = caps->isFormatTexturableAndUploadable(combo.fColorType,
                                                                         combo.fFormat);
                }

                sk_sp<GrSurface> tex = createTexture(kW, kH, combo.fColorType, combo.fFormat,
                                                     GrRenderable::kNo, resourceProvider);
                REPORTER_ASSERT(reporter, SkToBool(tex) == isTexturable,
                                "ct:%s format:%s, tex:%d, isTexturable:%d",
                                GrColorTypeToStr(combo.fColorType),
                                combo.fFormat.toStr().c_str(),
                                SkToBool(tex), isTexturable);

                // Check that the lack of mipmap support blocks the creation of mipmapped
                // proxies
                bool expectedMipMapability = isTexturable && caps->mipMapSupport() &&
                                              !caps->isFormatCompressed(combo.fFormat);

                sk_sp<GrTextureProxy> proxy = proxyProvider->createProxy(
                        combo.fFormat, desc, GrRenderable::kNo, 1, origin, GrMipMapped::kYes,
                        SkBackingFit::kExact, SkBudgeted::kNo, GrProtected::kNo);
                REPORTER_ASSERT(reporter, SkToBool(proxy.get()) == expectedMipMapability,
                                "ct:%s format:%s, tex:%d, expectedMipMapability:%d",
                                GrColorTypeToStr(combo.fColorType),
                                combo.fFormat.toStr().c_str(),
                                SkToBool(proxy.get()), expectedMipMapability);
            }

            // Check if 'isFormatAsColorTypeRenderable' agrees with 'createTexture' (w/o MSAA)
            {
                bool isRenderable = caps->isFormatRenderable(combo.fFormat, 1);

                sk_sp<GrSurface> tex = resourceProvider->createTexture(
                        desc, combo.fFormat, GrRenderable::kYes, 1, GrMipMapped::kNo,
                        SkBudgeted::kNo, GrProtected::kNo);
                REPORTER_ASSERT(reporter, SkToBool(tex) == isRenderable,
                                "ct:%s format:%s, tex:%d, isRenderable:%d",
                                GrColorTypeToStr(combo.fColorType),
                                combo.fFormat.toStr().c_str(),
                                SkToBool(tex), isRenderable);
            }

            // Check if 'isFormatAsColorTypeRenderable' agrees with 'createTexture' w/ MSAA
            {
                bool isRenderable = caps->isFormatRenderable(combo.fFormat, 2);

                sk_sp<GrSurface> tex = resourceProvider->createTexture(
                        desc, combo.fFormat, GrRenderable::kYes, 2, GrMipMapped::kNo,
                        SkBudgeted::kNo, GrProtected::kNo);
                REPORTER_ASSERT(reporter, SkToBool(tex) == isRenderable,
                                "ct:%s format:%s, tex:%d, isRenderable:%d",
                                GrColorTypeToStr(combo.fColorType),
                                combo.fFormat.toStr().c_str(),
                                SkToBool(tex), isRenderable);
            }
        }
    }
}

#include "src/gpu/GrDrawingManager.h"
#include "src/gpu/GrSurfaceProxy.h"
#include "src/gpu/GrTextureContext.h"

// For each context, set it to always clear the textures and then run through all the
// supported formats checking that the textures are actually cleared
DEF_GPUTEST(InitialTextureClear, reporter, baseOptions) {
    GrContextOptions options = baseOptions;
    options.fClearAllTextures = true;

    static constexpr int kSize = 100;
    static constexpr SkColor kClearColor = 0xABABABAB;

    const SkImageInfo info = SkImageInfo::Make(kSize, kSize, kRGBA_8888_SkColorType,
                                               kPremul_SkAlphaType);

    SkAutoPixmapStorage readback;
    readback.alloc(info);

    GrSurfaceDesc desc;
    desc.fWidth = desc.fHeight = kSize;

    for (int ct = 0; ct < sk_gpu_test::GrContextFactory::kContextTypeCnt; ++ct) {
        sk_gpu_test::GrContextFactory factory(options);
        auto contextType = static_cast<sk_gpu_test::GrContextFactory::ContextType>(ct);
        if (!sk_gpu_test::GrContextFactory::IsRenderingContext(contextType)) {
            continue;
        }
        auto context = factory.get(contextType);
        if (!context) {
            continue;
        }

        GrProxyProvider* proxyProvider = context->priv().proxyProvider();
        const GrCaps* caps = context->priv().caps();

        const std::vector<GrCaps::TestFormatColorTypeCombination>& combos =
                                                                    caps->getTestingCombinations();

        for (auto combo : combos) {

            SkASSERT(combo.fColorType != GrColorType::kUnknown);
            SkASSERT(combo.fFormat.isValid());

            if (!caps->isFormatTexturableAndUploadable(combo.fColorType, combo.fFormat)) {
                continue;
            }

            {
                GrPixelConfig config = caps->getConfigFromBackendFormat(combo.fFormat,
                                                                        combo.fColorType);
                SkASSERT(config != kUnknown_GrPixelConfig);

                desc.fConfig = config;
            }

            uint32_t components = GrColorTypeComponentFlags(combo.fColorType);
            uint32_t expectedClearColor =
                    (components & kAlpha_SkColorTypeComponentFlag) ? 0 : 0xFF000000;

            for (auto renderable : {GrRenderable::kNo, GrRenderable::kYes}) {
                if (renderable == GrRenderable::kYes &&
                    !caps->isFormatAsColorTypeRenderable(combo.fColorType, combo.fFormat)) {
                    continue;
                }

                for (auto fit : {SkBackingFit::kApprox, SkBackingFit::kExact}) {

                    // Does directly allocating a texture clear it?
                    {
                        auto proxy = proxyProvider->testingOnly_createInstantiatedProxy(
                                {kSize, kSize}, combo.fColorType, combo.fFormat, renderable, 1,
                                kTopLeft_GrSurfaceOrigin, fit, SkBudgeted::kYes, GrProtected::kNo);
                        if (proxy) {
                            auto texCtx = context->priv().makeWrappedSurfaceContext(
                                    std::move(proxy), combo.fColorType, kPremul_SkAlphaType);

                            readback.erase(kClearColor);
                            if (texCtx->readPixels(readback.info(), readback.writable_addr(),
                                                   readback.rowBytes(), {0, 0})) {
                                for (int i = 0; i < kSize * kSize; ++i) {
                                    if (expectedClearColor != readback.addr32()[i]) {
                                        ERRORF(reporter,
                                               "Failed on ct %s format %s 0x%x != 0x%x",
                                               GrColorTypeToStr(combo.fColorType),
                                               combo.fFormat.toStr().c_str(),
                                               expectedClearColor, readback.addr32()[i]);
                                        break;
                                    }
                                }
                            }
                        }

                        context->priv().testingOnly_purgeAllUnlockedResources();
                    }

                    // Try creating the texture as a deferred proxy.
                    {
                        std::unique_ptr<GrSurfaceContext> surfCtx;
                        if (renderable == GrRenderable::kYes) {
                            surfCtx = context->priv().makeDeferredRenderTargetContext(
                                    fit, desc.fWidth, desc.fHeight, combo.fColorType, nullptr,
                                    1, GrMipMapped::kNo, kTopLeft_GrSurfaceOrigin, nullptr);
                        } else {
                            surfCtx = context->priv().makeDeferredTextureContext(
                                    fit, desc.fWidth, desc.fHeight, combo.fColorType,
                                    kUnknown_SkAlphaType, nullptr, GrMipMapped::kNo,
                                    kTopLeft_GrSurfaceOrigin);
                        }
                        if (!surfCtx) {
                            continue;
                        }

                        readback.erase(kClearColor);
                        if (surfCtx->readPixels(readback.info(), readback.writable_addr(),
                                                readback.rowBytes(), {0, 0})) {
                            for (int i = 0; i < kSize * kSize; ++i) {
                                if (expectedClearColor != readback.addr32()[i]) {
                                    ERRORF(reporter, "Failed on ct %s format %s 0x%x != 0x%x",
                                           GrColorTypeToStr(combo.fColorType),
                                           combo.fFormat.toStr().c_str(),
                                           expectedClearColor, readback.addr32()[i]);
                                    break;
                                }
                            }
                        }
                        context->priv().testingOnly_purgeAllUnlockedResources();
                    }
                }
            }
        }
    }
}

DEF_GPUTEST_FOR_RENDERING_CONTEXTS(ReadOnlyTexture, reporter, context_info) {
    auto fillPixels = [](SkPixmap* p, const std::function<uint32_t(int x, int y)>& f) {
        for (int y = 0; y < p->height(); ++y) {
            for (int x = 0; x < p->width(); ++x) {
                *p->writable_addr32(x, y) = f(x, y);
            }
        }
    };

    auto comparePixels = [](const SkPixmap& p1, const SkPixmap& p2, skiatest::Reporter* reporter) {
        SkASSERT(p1.info() == p2.info());
        for (int y = 0; y < p1.height(); ++y) {
            for (int x = 0; x < p1.width(); ++x) {
                REPORTER_ASSERT(reporter, p1.getColor(x, y) == p2.getColor(x, y));
                if (p1.getColor(x, y) != p2.getColor(x, y)) {
                    return;
                }
            }
        }
    };

    static constexpr int kSize = 100;
    SkImageInfo ii = SkImageInfo::Make(kSize, kSize, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
    SkAutoPixmapStorage srcPixmap;
    srcPixmap.alloc(ii);
    fillPixels(&srcPixmap,
               [](int x, int y) {
                    return (0xFFU << 24) | (x << 16) | (y << 8) | uint8_t((x * y) & 0xFF);
               });

    GrContext* context = context_info.grContext();
    GrProxyProvider* proxyProvider = context->priv().proxyProvider();

    // We test both kRW in addition to kRead mostly to ensure that the calls are structured such
    // that they'd succeed if the texture wasn't kRead. We want to be sure we're failing with
    // kRead for the right reason.
    for (auto ioType : {kRead_GrIOType, kRW_GrIOType}) {
        auto backendTex = context->priv().createBackendTexture(&srcPixmap, 1,
                                                               GrRenderable::kYes,
                                                               GrProtected::kNo);

        auto proxy = proxyProvider->wrapBackendTexture(backendTex, GrColorType::kRGBA_8888,
                                                       kTopLeft_GrSurfaceOrigin,
                                                       kBorrow_GrWrapOwnership,
                                                       GrWrapCacheable::kNo, ioType);
        auto surfContext = context->priv().makeWrappedSurfaceContext(proxy, GrColorType::kRGBA_8888,
                                                                     kPremul_SkAlphaType);

        // Read pixels should work with a read-only texture.
        {
            SkAutoPixmapStorage read;
            read.alloc(srcPixmap.info());
            auto readResult = surfContext->readPixels(srcPixmap.info(), read.writable_addr(),
                                                      0, { 0, 0 });
            REPORTER_ASSERT(reporter, readResult);
            if (readResult) {
                comparePixels(srcPixmap, read, reporter);
            }
        }

        // Write pixels should not work with a read-only texture.
        SkAutoPixmapStorage write;
        write.alloc(srcPixmap.info());
        fillPixels(&write, [&srcPixmap](int x, int y) { return ~*srcPixmap.addr32(); });
        auto writeResult = surfContext->writePixels(srcPixmap.info(), write.addr(), 0, {0, 0});
        REPORTER_ASSERT(reporter, writeResult == (ioType == kRW_GrIOType));
        // Try the low level write.
        context->flush();
        auto gpuWriteResult = context->priv().getGpu()->writePixels(
                proxy->peekTexture(), 0, 0, kSize, kSize, GrColorType::kRGBA_8888,
                GrColorType::kRGBA_8888, write.addr32(),
                kSize * GrColorTypeBytesPerPixel(GrColorType::kRGBA_8888));
        REPORTER_ASSERT(reporter, gpuWriteResult == (ioType == kRW_GrIOType));

        // Copies should not work with a read-only texture
        auto copySrc =
                proxyProvider->createTextureProxy(SkImage::MakeFromRaster(write, nullptr, nullptr),
                                                  1, SkBudgeted::kYes, SkBackingFit::kExact);
        REPORTER_ASSERT(reporter, copySrc);
        auto copyResult = surfContext->testCopy(copySrc.get());
        REPORTER_ASSERT(reporter, copyResult == (ioType == kRW_GrIOType));
        // Try the low level copy.
        context->flush();
        auto gpuCopyResult = context->priv().getGpu()->copySurface(
                proxy->peekTexture(), copySrc->peekTexture(), SkIRect::MakeWH(kSize, kSize),
                {0, 0});
        REPORTER_ASSERT(reporter, gpuCopyResult == (ioType == kRW_GrIOType));

        // Mip regen should not work with a read only texture.
        if (context->priv().caps()->mipMapSupport()) {
            delete_backend_texture(context, backendTex);
            backendTex = context->createBackendTexture(
                    kSize, kSize, kRGBA_8888_SkColorType,
                    SkColors::kTransparent, GrMipMapped::kYes, GrRenderable::kYes,
                    GrProtected::kNo);
            proxy = proxyProvider->wrapBackendTexture(backendTex, GrColorType::kRGBA_8888,
                                                      kTopLeft_GrSurfaceOrigin,
                                                      kBorrow_GrWrapOwnership, GrWrapCacheable::kNo,
                                                      ioType);
            context->flush();
            proxy->peekTexture()->texturePriv().markMipMapsDirty();  // avoids assert in GrGpu.
            auto regenResult =
                    context->priv().getGpu()->regenerateMipMapLevels(proxy->peekTexture());
            REPORTER_ASSERT(reporter, regenResult == (ioType == kRW_GrIOType));
        }
        delete_backend_texture(context, backendTex);
    }
}

static const int kSurfSize = 10;

static sk_sp<GrTexture> make_wrapped_texture(GrContext* context, GrRenderable renderable) {
    auto backendTexture = context->createBackendTexture(
            kSurfSize, kSurfSize, kRGBA_8888_SkColorType, SkColors::kTransparent, GrMipMapped::kNo,
            renderable, GrProtected::kNo);
    sk_sp<GrTexture> texture;
    if (GrRenderable::kYes == renderable) {
        texture = context->priv().resourceProvider()->wrapRenderableBackendTexture(
                backendTexture, 1, GrColorType::kRGBA_8888, kBorrow_GrWrapOwnership,
                GrWrapCacheable::kNo);
    } else {
        texture = context->priv().resourceProvider()->wrapBackendTexture(
                backendTexture, GrColorType::kRGBA_8888, kBorrow_GrWrapOwnership,
                GrWrapCacheable::kNo, kRW_GrIOType);
    }
    // Add a release proc that deletes the GrBackendTexture.
    struct ReleaseContext {
        GrContext* fContext;
        GrBackendTexture fBackendTexture;
    };
    auto release = [](void* rc) {
        auto releaseContext = static_cast<ReleaseContext*>(rc);
        auto context = releaseContext->fContext;
        context->deleteBackendTexture(releaseContext->fBackendTexture);
        delete releaseContext;
    };
    texture->setRelease(release, new ReleaseContext{context, backendTexture});
    return texture;
}

static sk_sp<GrTexture> make_normal_texture(GrContext* context, GrRenderable renderable) {
    GrSurfaceDesc desc;
    desc.fConfig = kRGBA_8888_GrPixelConfig;
    desc.fWidth = desc.fHeight = kSurfSize;
    auto format =
            context->priv().caps()->getDefaultBackendFormat(GrColorType::kRGBA_8888, renderable);
    return context->priv().resourceProvider()->createTexture(
            desc, format, renderable, 1, GrMipMapped::kNo, SkBudgeted::kNo, GrProtected::kNo);
}

DEF_GPUTEST(TextureIdleProcTest, reporter, options) {
    // Various ways of making textures.
    auto makeWrapped = [](GrContext* context) {
        return make_wrapped_texture(context, GrRenderable::kNo);
    };
    auto makeWrappedRenderable = [](GrContext* context) {
        return make_wrapped_texture(context, GrRenderable::kYes);
    };
    auto makeNormal = [](GrContext* context) {
        return make_normal_texture(context, GrRenderable::kNo);
    };
    auto makeRenderable = [](GrContext* context) {
        return make_normal_texture(context, GrRenderable::kYes);
    };

    std::function<sk_sp<GrTexture>(GrContext*)> makers[] = {makeWrapped, makeWrappedRenderable,
                                                            makeNormal, makeRenderable};

    // Add a unique key, or not.
    auto addKey = [](GrTexture* texture) {
        static uint32_t gN = 0;
        static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
        GrUniqueKey key;
        GrUniqueKey::Builder builder(&key, kDomain, 1);
        builder[0] = gN++;
        builder.finish();
        texture->resourcePriv().setUniqueKey(key);
    };
    auto dontAddKey = [](GrTexture* texture) {};
    std::function<void(GrTexture*)> keyAdders[] = {addKey, dontAddKey};

    for (const auto& m : makers) {
        for (const auto& keyAdder : keyAdders) {
            for (int type = 0; type < sk_gpu_test::GrContextFactory::kContextTypeCnt; ++type) {
                sk_gpu_test::GrContextFactory factory;
                auto contextType = static_cast<sk_gpu_test::GrContextFactory::ContextType>(type);
                GrContext* context = factory.get(contextType);
                if (!context) {
                    continue;
                }

                // The callback we add simply adds an integer to a set.
                std::set<int> idleIDs;
                struct Context {
                    std::set<int>* fIdleIDs;
                    int fNum;
                };
                auto proc = [](void* context) {
                    static_cast<Context*>(context)->fIdleIDs->insert(
                            static_cast<Context*>(context)->fNum);
                    delete static_cast<Context*>(context);
                };

                // Makes a texture, possibly adds a key, and sets the callback.
                auto make = [&m, &keyAdder, &proc, &idleIDs](GrContext* context, int num) {
                    sk_sp<GrTexture> texture = m(context);
                    texture->addIdleProc(proc, new Context{&idleIDs, num},
                                         GrTexture::IdleState::kFinished);
                    keyAdder(texture.get());
                    return texture;
                };

                auto texture = make(context, 1);
                REPORTER_ASSERT(reporter, idleIDs.find(1) == idleIDs.end());
                auto renderable = GrRenderable(SkToBool(texture->asRenderTarget()));
                auto backendFormat = texture->backendFormat();
                texture.reset();
                REPORTER_ASSERT(reporter, idleIDs.find(1) != idleIDs.end());

                texture = make(context, 2);
                int w = texture->width();
                int h = texture->height();
                SkImageInfo info =
                        SkImageInfo::Make(w, h, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
                auto rt = SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info, 0, nullptr);
                auto rtc = rt->getCanvas()->internal_private_accessTopLayerRenderTargetContext();
                auto singleUseLazyCB = [&texture](GrResourceProvider* rp) {
                    auto mode = GrSurfaceProxy::LazyInstantiationKeyMode::kSynced;
                    if (texture->getUniqueKey().isValid()) {
                        mode = GrSurfaceProxy::LazyInstantiationKeyMode::kUnsynced;
                    }
                    return GrSurfaceProxy::LazyCallbackResult{std::move(texture), true, mode};
                };
                GrSurfaceDesc desc;
                desc.fWidth = w;
                desc.fHeight = h;
                desc.fConfig = kRGBA_8888_GrPixelConfig;
                SkBudgeted budgeted;
                if (texture->resourcePriv().budgetedType() == GrBudgetedType::kBudgeted) {
                    budgeted = SkBudgeted::kYes;
                } else {
                    budgeted = SkBudgeted::kNo;
                }
                auto proxy = context->priv().proxyProvider()->createLazyProxy(
                        singleUseLazyCB, backendFormat, desc, renderable, 1,
                        GrSurfaceOrigin::kTopLeft_GrSurfaceOrigin, GrMipMapped::kNo,
                        GrMipMapsStatus::kNotAllocated, GrInternalSurfaceFlags ::kNone,
                        SkBackingFit::kExact, budgeted, GrProtected::kNo,
                        GrSurfaceProxy::UseAllocator::kYes);
                rtc->drawTexture(GrNoClip(), proxy, GrSamplerState::Filter::kNearest,
                                 SkBlendMode::kSrcOver, SkPMColor4f(), SkRect::MakeWH(w, h),
                                 SkRect::MakeWH(w, h), GrAA::kNo, GrQuadAAFlags::kNone,
                                 SkCanvas::kFast_SrcRectConstraint, SkMatrix::I(), nullptr);
                // We still have the proxy, which should remain instantiated, thereby keeping the
                // texture not purgeable.
                REPORTER_ASSERT(reporter, idleIDs.find(2) == idleIDs.end());
                context->flush();
                REPORTER_ASSERT(reporter, idleIDs.find(2) == idleIDs.end());
                context->priv().getGpu()->testingOnly_flushGpuAndSync();
                REPORTER_ASSERT(reporter, idleIDs.find(2) == idleIDs.end());

                // This time we move the proxy into the draw.
                rtc->drawTexture(GrNoClip(), std::move(proxy), GrSamplerState::Filter::kNearest,
                                 SkBlendMode::kSrcOver, SkPMColor4f(), SkRect::MakeWH(w, h),
                                 SkRect::MakeWH(w, h), GrAA::kNo, GrQuadAAFlags::kNone,
                                 SkCanvas::kFast_SrcRectConstraint, SkMatrix::I(), nullptr);
                REPORTER_ASSERT(reporter, idleIDs.find(2) == idleIDs.end());
                context->flush();
                context->priv().getGpu()->testingOnly_flushGpuAndSync();
                // Now that the draw is fully consumed by the GPU, the texture should be idle.
                REPORTER_ASSERT(reporter, idleIDs.find(2) != idleIDs.end());

                // Make sure we make the call during various shutdown scenarios where the texture
                // might persist after context is destroyed, abandoned, etc. We test three
                // variations of each scenario. One where the texture is just created. Another,
                // where the texture has been used in a draw and then the context is flushed. And
                // one where the the texture was drawn but the context is not flushed.
                // In each scenario we test holding a ref beyond the context shutdown and not.

                // These tests are difficult to get working with Vulkan. See http://skbug.com/8705
                // and http://skbug.com/8275
                GrBackendApi api = sk_gpu_test::GrContextFactory::ContextTypeBackend(contextType);
                if (api == GrBackendApi::kVulkan) {
                    continue;
                }
                int id = 3;
                enum class DrawType {
                    kNoDraw,
                    kDraw,
                    kDrawAndFlush,
                };
                for (auto drawType :
                     {DrawType::kNoDraw, DrawType::kDraw, DrawType::kDrawAndFlush}) {
                    for (bool unrefFirst : {false, true}) {
                        auto possiblyDrawAndFlush = [&context, &texture, drawType, unrefFirst, w,
                                                     h] {
                            if (drawType == DrawType::kNoDraw) {
                                return;
                            }
                            SkImageInfo info = SkImageInfo::Make(w, h, kRGBA_8888_SkColorType,
                                                                 kPremul_SkAlphaType);
                            auto rt = SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info, 0,
                                                                  nullptr);
                            auto rtc = rt->getCanvas()
                                            ->internal_private_accessTopLayerRenderTargetContext();
                            auto proxy = context->priv().proxyProvider()->testingOnly_createWrapped(
                                    texture, GrColorType::kRGBA_8888, kTopLeft_GrSurfaceOrigin);
                            rtc->drawTexture(
                                    GrNoClip(), proxy, GrSamplerState::Filter::kNearest,
                                    SkBlendMode::kSrcOver, SkPMColor4f(), SkRect::MakeWH(w, h),
                                    SkRect::MakeWH(w, h), GrAA::kNo, GrQuadAAFlags::kNone,
                                    SkCanvas::kFast_SrcRectConstraint, SkMatrix::I(), nullptr);
                            if (drawType == DrawType::kDrawAndFlush) {
                                context->flush();
                            }
                            if (unrefFirst) {
                                texture.reset();
                            }
                        };
                        texture = make(context, id);
                        possiblyDrawAndFlush();
                        context->abandonContext();
                        texture.reset();
                        REPORTER_ASSERT(reporter, idleIDs.find(id) != idleIDs.end());
                        factory.destroyContexts();
                        context = factory.get(contextType);
                        ++id;

                        // Similar to previous, but reset the texture after the context was
                        // abandoned and then destroyed.
                        texture = make(context, id);
                        possiblyDrawAndFlush();
                        context->abandonContext();
                        factory.destroyContexts();
                        texture.reset();
                        REPORTER_ASSERT(reporter, idleIDs.find(id) != idleIDs.end());
                        context = factory.get(contextType);
                        id++;

                        texture = make(context, id);
                        possiblyDrawAndFlush();
                        factory.destroyContexts();
                        texture.reset();
                        REPORTER_ASSERT(reporter, idleIDs.find(id) != idleIDs.end());
                        context = factory.get(contextType);
                        id++;

                        texture = make(context, id);
                        possiblyDrawAndFlush();
                        factory.releaseResourcesAndAbandonContexts();
                        texture.reset();
                        REPORTER_ASSERT(reporter, idleIDs.find(id) != idleIDs.end());
                        context = factory.get(contextType);
                        id++;
                    }
                }
            }
        }
    }
}

// Tests an idle proc that unrefs another resource down to zero.
DEF_GPUTEST_FOR_ALL_CONTEXTS(TextureIdleProcCacheManipulationTest, reporter, contextInfo) {
    GrContext* context = contextInfo.grContext();

    // idle proc that releases another texture.
    auto idleProc = [](void* texture) { reinterpret_cast<GrTexture*>(texture)->unref(); };

    for (const auto& idleMaker : {make_wrapped_texture, make_normal_texture}) {
        for (const auto& otherMaker : {make_wrapped_texture, make_normal_texture}) {
            for (auto idleState :
                 {GrTexture::IdleState::kFlushed, GrTexture::IdleState::kFinished}) {
                auto idleTexture = idleMaker(context, GrRenderable::kNo);
                auto otherTexture = otherMaker(context, GrRenderable::kNo);
                otherTexture->ref();
                idleTexture->addIdleProc(idleProc, otherTexture.get(), idleState);
                otherTexture.reset();
                idleTexture.reset();
            }
        }
    }
}

// Similar to above but more complicated. This flushes the context from the idle proc.
// crbug.com/933526.
DEF_GPUTEST_FOR_ALL_CONTEXTS(TextureIdleProcFlushTest, reporter, contextInfo) {
    GrContext* context = contextInfo.grContext();

    // idle proc that flushes the context.
    auto idleProc = [](void* context) { reinterpret_cast<GrContext*>(context)->flush(); };

    for (const auto& idleMaker : {make_wrapped_texture, make_normal_texture}) {
        for (auto idleState : {GrTexture::IdleState::kFlushed, GrTexture::IdleState::kFinished}) {
            auto idleTexture = idleMaker(context, GrRenderable::kNo);
            idleTexture->addIdleProc(idleProc, context, idleState);
            auto info = SkImageInfo::Make(10, 10, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
            auto surf = SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info, 1, nullptr);
            // We'll draw two images to the canvas. One is a normal texture-backed image. The other
            // is a wrapped-texture backed image.
            surf->getCanvas()->clear(SK_ColorWHITE);
            auto img1 = surf->makeImageSnapshot();

            GrBackendTexture backendTexture;

            if (!create_backend_texture(context, &backendTexture, info, SkColors::kBlack,
                                        GrMipMapped::kNo, GrRenderable::kNo)) {
                REPORTER_ASSERT(reporter, false);
                continue;
            }

            auto img2 = SkImage::MakeFromTexture(context, backendTexture, kTopLeft_GrSurfaceOrigin,
                                                 info.colorType(), info.alphaType(), nullptr);
            surf->getCanvas()->drawImage(std::move(img1), 0, 0);
            surf->getCanvas()->drawImage(std::move(img2), 1, 1);
            idleTexture.reset();

            delete_backend_texture(context, backendTexture);
        }
    }
}

DEF_GPUTEST_FOR_ALL_CONTEXTS(TextureIdleProcRerefTest, reporter, contextInfo) {
    GrContext* context = contextInfo.grContext();
    // idle proc that refs the texture
    auto idleProc = [](void* texture) { reinterpret_cast<GrTexture*>(texture)->ref(); };
    // release proc to check whether the texture was released or not.
    auto releaseProc = [](void* isReleased) { *reinterpret_cast<bool*>(isReleased) = true; };
    for (auto idleState : {GrTexture::IdleState::kFlushed, GrTexture::IdleState::kFinished}) {
        bool isReleased = false;
        auto idleTexture = make_normal_texture(context, GrRenderable::kNo);
        // This test assumes the texture won't be cached (or else the release proc doesn't get
        // called).
        idleTexture->resourcePriv().removeScratchKey();
        context->flush();
        idleTexture->addIdleProc(idleProc, idleTexture.get(), idleState);
        idleTexture->setRelease(releaseProc, &isReleased);
        auto* raw = idleTexture.get();
        idleTexture.reset();
        REPORTER_ASSERT(reporter, !isReleased);
        raw->unref();
        REPORTER_ASSERT(reporter, isReleased);
    }
}

DEF_GPUTEST_FOR_ALL_CONTEXTS(TextureIdleStateTest, reporter, contextInfo) {
    GrContext* context = contextInfo.grContext();
    for (const auto& idleMaker : {make_wrapped_texture, make_normal_texture}) {
        auto idleTexture = idleMaker(context, GrRenderable::kNo);

        uint32_t flags = 0;
        static constexpr uint32_t kFlushFlag = 0x1;
        static constexpr uint32_t kFinishFlag = 0x2;
        auto flushProc = [](void* flags) { *static_cast<uint32_t*>(flags) |= kFlushFlag; };
        auto finishProc = [](void* flags) { *static_cast<uint32_t*>(flags) |= kFinishFlag; };
        idleTexture->addIdleProc(flushProc, &flags, GrTexture::IdleState::kFlushed);
        idleTexture->addIdleProc(finishProc, &flags, GrTexture::IdleState::kFinished);

        // Insert a copy from idleTexture to another texture so that we have some queued IO on
        // idleTexture.
        SkImageInfo info = SkImageInfo::Make(kSurfSize, kSurfSize, kRGBA_8888_SkColorType,
                                             kPremul_SkAlphaType);
        auto rt = SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info, 0, nullptr);
        auto rtc = rt->getCanvas()->internal_private_accessTopLayerRenderTargetContext();
        auto proxy = context->priv().proxyProvider()->testingOnly_createWrapped(
                std::move(idleTexture), GrColorType::kRGBA_8888, rtc->asSurfaceProxy()->origin());
        context->flush();
        SkAssertResult(rtc->testCopy(proxy.get()));
        proxy.reset();
        REPORTER_ASSERT(reporter, flags == 0);

        // After a flush we expect idleTexture to have reached the kFlushed state on all backends.
        // On "managed" backends we expect it to reach kFinished as well. On Vulkan, the only
        // current "unmanaged" backend, we *may* need a sync to reach kFinished.
        context->flush();
        if (contextInfo.backend() == kVulkan_GrBackend) {
            REPORTER_ASSERT(reporter, flags & kFlushFlag);
        } else {
            REPORTER_ASSERT(reporter, flags == (kFlushFlag | kFinishFlag));
        }
        context->priv().getGpu()->testingOnly_flushGpuAndSync();
        REPORTER_ASSERT(reporter, flags == (kFlushFlag | kFinishFlag));
    }
}