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

#include "include/core/SkAlphaType.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.h"
#include "include/core/SkData.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkSurface.h"
#include "include/core/SkSurfaceProps.h"
#include "include/core/SkTypes.h"
#include "include/gpu/GpuTypes.h"
#include "include/gpu/GrBackendSurface.h"
#include "include/gpu/GrDirectContext.h"
#include "include/gpu/GrRecordingContext.h"
#include "include/gpu/GrTypes.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "include/private/SkColorData.h"
#include "include/private/base/SkDebug.h"
#include "include/private/base/SkMalloc.h"
#include "include/private/chromium/GrDeferredDisplayList.h"
#include "include/private/chromium/GrDeferredDisplayListRecorder.h"
#include "include/private/chromium/GrSurfaceCharacterization.h"
#include "include/private/gpu/ganesh/GrTypesPriv.h"
#include "src/base/SkRandom.h"
#include "src/core/SkMessageBus.h"
#include "src/gpu/GpuTypesPriv.h"
#include "src/gpu/ResourceKey.h"
#include "src/gpu/SkBackingFit.h"
#include "src/gpu/Swizzle.h"
#include "src/gpu/ganesh/GrAppliedClip.h"
#include "src/gpu/ganesh/GrBuffer.h"
#include "src/gpu/ganesh/GrCanvas.h"
#include "src/gpu/ganesh/GrCaps.h"
#include "src/gpu/ganesh/GrColorSpaceXform.h"
#include "src/gpu/ganesh/GrDefaultGeoProcFactory.h"
#include "src/gpu/ganesh/GrDirectContextPriv.h"
#include "src/gpu/ganesh/GrGpu.h"
#include "src/gpu/ganesh/GrGpuBuffer.h"
#include "src/gpu/ganesh/GrOpFlushState.h"
#include "src/gpu/ganesh/GrPaint.h"
#include "src/gpu/ganesh/GrProcessorSet.h"
#include "src/gpu/ganesh/GrProxyProvider.h"
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
#include "src/gpu/ganesh/GrRenderTargetProxy.h"
#include "src/gpu/ganesh/GrResourceCache.h"
#include "src/gpu/ganesh/GrResourceProvider.h"
#include "src/gpu/ganesh/GrSamplerState.h"
#include "src/gpu/ganesh/GrStyle.h"
#include "src/gpu/ganesh/GrSurface.h"
#include "src/gpu/ganesh/GrSurfaceProxy.h"
#include "src/gpu/ganesh/GrSurfaceProxyView.h"
#include "src/gpu/ganesh/GrTextureProxy.h"
#include "src/gpu/ganesh/GrThreadSafeCache.h"
#include "src/gpu/ganesh/SurfaceDrawContext.h"
#include "src/gpu/ganesh/ops/GrDrawOp.h"
#include "src/gpu/ganesh/ops/GrOp.h"
#include "tests/CtsEnforcement.h"
#include "tests/Test.h"
#include "tests/TestUtils.h"
#include "tools/gpu/ProxyUtils.h"

#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <thread>
#include <utility>

class GrDstProxyView;
class GrProgramInfo;
class GrThreadSafeVertexTestOp;
class SkArenaAlloc;
enum class GrXferBarrierFlags;
struct GrContextOptions;

static constexpr int kImageWH = 32;
static constexpr auto kImageOrigin = kBottomLeft_GrSurfaceOrigin;
static constexpr int kNoID = -1;

static SkImageInfo default_ii(int wh) {
    return SkImageInfo::Make(wh, wh, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
}

static std::unique_ptr<skgpu::ganesh::SurfaceDrawContext> new_SDC(GrRecordingContext* rContext,
                                                                  int wh) {
    return skgpu::ganesh::SurfaceDrawContext::Make(rContext,
                                                   GrColorType::kRGBA_8888,
                                                   nullptr,
                                                   SkBackingFit::kExact,
                                                   {wh, wh},
                                                   SkSurfaceProps(),
                                                   /* label= */ {},
                                                   /* sampleCnt= */ 1,
                                                   skgpu::Mipmapped::kNo,
                                                   GrProtected::kNo,
                                                   kImageOrigin,
                                                   skgpu::Budgeted::kYes);
}

static void create_view_key(skgpu::UniqueKey* key, int wh, int id) {
    static const skgpu::UniqueKey::Domain kViewDomain = skgpu::UniqueKey::GenerateDomain();
    skgpu::UniqueKey::Builder builder(key, kViewDomain, 1);
    builder[0] = wh;
    builder.finish();

    if (id != kNoID) {
        key->setCustomData(SkData::MakeWithCopy(&id, sizeof(id)));
    }
}

static void create_vert_key(skgpu::UniqueKey* key, int wh, int id) {
    static const skgpu::UniqueKey::Domain kVertDomain = skgpu::UniqueKey::GenerateDomain();
    skgpu::UniqueKey::Builder builder(key, kVertDomain, 1);
    builder[0] = wh;
    builder.finish();

    if (id != kNoID) {
        key->setCustomData(SkData::MakeWithCopy(&id, sizeof(id)));
    }
}

static bool default_is_newer_better(SkData* incumbent, SkData* challenger) {
    return false;
}

// When testing views we create a bitmap that covers the entire screen and has an inset blue rect
// atop a field of white.
// When testing verts we clear the background to white and simply draw an inset blur rect.
static SkBitmap create_bitmap(int wh) {
    SkBitmap bitmap;

    bitmap.allocPixels(default_ii(wh));

    SkCanvas tmp(bitmap);
    tmp.clear(SK_ColorWHITE);

    SkPaint blue;
    blue.setColor(SK_ColorBLUE);
    blue.setAntiAlias(false);

    tmp.drawRect({10, 10, wh-10.0f, wh-10.0f}, blue);

    bitmap.setImmutable();
    return bitmap;
}

class TestHelper {
public:
    struct Stats {
        int fCacheHits = 0;
        int fCacheMisses = 0;

        int fNumSWCreations = 0;
        int fNumLazyCreations = 0;
        int fNumHWCreations = 0;
    };

    TestHelper(GrDirectContext* dContext,
               GrThreadSafeCache::IsNewerBetter isNewerBetter = default_is_newer_better)
            : fDContext(dContext)
            , fIsNewerBetter(isNewerBetter) {
        fDst = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, default_ii(kImageWH));
        SkAssertResult(fDst);

        GrSurfaceCharacterization characterization;
        SkAssertResult(fDst->characterize(&characterization));

        fRecorder1 = std::make_unique<GrDeferredDisplayListRecorder>(characterization);
        this->ddlCanvas1()->clear(SkColors::kWhite);

        fRecorder2 = std::make_unique<GrDeferredDisplayListRecorder>(characterization);
        this->ddlCanvas2()->clear(SkColors::kWhite);

        fDst->getCanvas()->clear(SkColors::kWhite);
    }

    ~TestHelper() {
        fDContext->flush();
        fDContext->submit(GrSyncCpu::kYes);
    }

    Stats* stats() { return &fStats; }

    int numCacheEntries() const { return this->threadSafeCache()->numEntries(); }

    GrDirectContext* dContext() { return fDContext; }

    SkCanvas* liveCanvas() { return fDst ? fDst->getCanvas() : nullptr; }
    SkCanvas* ddlCanvas1() { return fRecorder1 ? fRecorder1->getCanvas() : nullptr; }
    sk_sp<GrDeferredDisplayList> snap1() {
        if (fRecorder1) {
            sk_sp<GrDeferredDisplayList> tmp = fRecorder1->detach();
            fRecorder1 = nullptr;
            return tmp;
        }

        return nullptr;
    }
    SkCanvas* ddlCanvas2() { return fRecorder2 ? fRecorder2->getCanvas() : nullptr; }
    sk_sp<GrDeferredDisplayList> snap2() {
        if (fRecorder2) {
            sk_sp<GrDeferredDisplayList> tmp = fRecorder2->detach();
            fRecorder2 = nullptr;
            return tmp;
        }

        return nullptr;
    }

    GrThreadSafeCache* threadSafeCache() { return fDContext->priv().threadSafeCache(); }
    const GrThreadSafeCache* threadSafeCache() const { return fDContext->priv().threadSafeCache(); }

    typedef void (TestHelper::*addAccessFP)(SkCanvas*, int wh, int id,
                                            bool failLookUp, bool failFillingIn);
    typedef bool (TestHelper::*checkFP)(SkCanvas*, int wh,
                                        int expectedHits, int expectedMisses,
                                        int expectedNumRefs, int expectedID);

    // Add a draw on 'canvas' that will introduce a ref on the 'wh' view
    void addViewAccess(SkCanvas* canvas,
                       int wh,
                       int id = kNoID,
                       bool failLookup = false,
                       bool failFillingIn = false) {
        auto rContext = canvas->recordingContext();

        auto view = AccessCachedView(rContext, this->threadSafeCache(),
                                     wh, failLookup, failFillingIn, id, &fStats);
        SkASSERT(view);

        auto sdc = skgpu::ganesh::TopDeviceSurfaceDrawContext(canvas);

        sdc->drawTexture(nullptr,
                         view,
                         kPremul_SkAlphaType,
                         GrSamplerState::Filter::kNearest,
                         GrSamplerState::MipmapMode::kNone,
                         SkBlendMode::kSrcOver,
                         {1.0f, 1.0f, 1.0f, 1.0f},
                         SkRect::MakeWH(wh, wh),
                         SkRect::MakeWH(wh, wh),
                         GrQuadAAFlags::kNone,
                         SkCanvas::kFast_SrcRectConstraint,
                         SkMatrix::I(),
                         nullptr);
    }

    // Besides checking that the number of refs and cache hits and misses are as expected, this
    // method also validates that the unique key doesn't appear in any of the other caches.
    bool checkView(SkCanvas* canvas, int wh,
                   int expectedHits, int expectedMisses, int expectedNumRefs, int expectedID) {
        if (fStats.fCacheHits != expectedHits || fStats.fCacheMisses != expectedMisses) {
            SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n",
                     expectedHits, fStats.fCacheHits, expectedMisses, fStats.fCacheMisses);
            return false;
        }

        skgpu::UniqueKey key;
        create_view_key(&key, wh, kNoID);

        auto threadSafeCache = this->threadSafeCache();

        auto [view, xtraData] = threadSafeCache->findWithData(key);
        if (!view.proxy()) {
            return false;
        }

        if (expectedID < 0) {
            if (xtraData) {
                return false;
            }
        } else {
            if (!xtraData) {
                return false;
            }

            const int* cachedID = static_cast<const int*>(xtraData->data());
            if (*cachedID != expectedID) {
                return false;
            }
        }

        if (!view.proxy()->refCntGreaterThan(expectedNumRefs+1) ||  // +1 for 'view's ref
            view.proxy()->refCntGreaterThan(expectedNumRefs+2)) {
            return false;
        }

        if (canvas) {
            GrRecordingContext* rContext = canvas->recordingContext();
            GrProxyProvider* recordingProxyProvider = rContext->priv().proxyProvider();
            sk_sp<GrTextureProxy> result = recordingProxyProvider->findProxyByUniqueKey(key);
            if (result) {
                // views in this cache should never appear in the recorder's cache
                return false;
            }
        }

        {
            GrProxyProvider* directProxyProvider = fDContext->priv().proxyProvider();
            sk_sp<GrTextureProxy> result = directProxyProvider->findProxyByUniqueKey(key);
            if (result) {
                // views in this cache should never appear in the main proxy cache
                return false;
            }
        }

        {
            auto resourceProvider = fDContext->priv().resourceProvider();
            sk_sp<GrSurface> surf = resourceProvider->findByUniqueKey<GrSurface>(key);
            if (surf) {
                // the textures backing the views in this cache should never be discoverable in the
                // resource cache
                return false;
            }
        }

        return true;
    }

    void addVertAccess(SkCanvas* canvas,
                       int wh,
                       int id,
                       bool failLookup,
                       bool failFillingIn,
                       GrThreadSafeVertexTestOp** createdOp);

    // Add a draw on 'canvas' that will introduce a ref on a 'wh' vertex data
    void addVertAccess(SkCanvas* canvas,
                       int wh,
                       int id = kNoID,
                       bool failLookup = false,
                       bool failFillingIn = false) {
        this->addVertAccess(canvas, wh, id, failLookup, failFillingIn, nullptr);
    }

    bool checkVert(SkCanvas* canvas, int wh,
                   int expectedHits, int expectedMisses, int expectedNumRefs, int expectedID) {
        if (fStats.fCacheHits != expectedHits || fStats.fCacheMisses != expectedMisses) {
            SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n",
                     expectedHits, fStats.fCacheHits, expectedMisses, fStats.fCacheMisses);
            return false;
        }

        skgpu::UniqueKey key;
        create_vert_key(&key, wh, kNoID);

        auto threadSafeCache = this->threadSafeCache();

        auto [vertData, xtraData] = threadSafeCache->findVertsWithData(key);
        if (!vertData) {
            return false;
        }

        if (expectedID < 0) {
            if (xtraData) {
                return false;
            }
        } else {
            if (!xtraData) {
                return false;
            }

            const int* cachedID = static_cast<const int*>(xtraData->data());
            if (*cachedID != expectedID) {
                return false;
            }
        }

        if (!vertData->refCntGreaterThan(expectedNumRefs+1) ||  // +1 for 'vertData's ref
            vertData->refCntGreaterThan(expectedNumRefs+2)) {
            return false;
        }

        {
            auto resourceProvider = fDContext->priv().resourceProvider();
            sk_sp<GrGpuBuffer> buffer = resourceProvider->findByUniqueKey<GrGpuBuffer>(key);
            if (buffer) {
                // the buffer holding the vertex data in this cache should never be discoverable
                // in the resource cache
                return false;
            }
        }

        return true;
    }

    bool checkImage(skiatest::Reporter* reporter, const sk_sp<SkSurface>& s) {
        SkBitmap actual;

        actual.allocPixels(default_ii(kImageWH));

        if (!s->readPixels(actual, 0, 0)) {
            return false;
        }

        SkBitmap expected = create_bitmap(kImageWH);

        const float tols[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

        auto error = std::function<ComparePixmapsErrorReporter>(
            [reporter](int x, int y, const float diffs[4]) {
                SkASSERT(x >= 0 && y >= 0);
                ERRORF(reporter, "mismatch at %d, %d (%f, %f, %f %f)",
                       x, y, diffs[0], diffs[1], diffs[2], diffs[3]);
            });

        return ComparePixels(expected.pixmap(), actual.pixmap(), tols, error);
    }

    bool checkImage(skiatest::Reporter* reporter) {
        return this->checkImage(reporter, fDst);
    }

    bool checkImage(skiatest::Reporter* reporter, sk_sp<GrDeferredDisplayList> ddl) {
        sk_sp<SkSurface> tmp =
                SkSurfaces::RenderTarget(fDContext, skgpu::Budgeted::kNo, default_ii(kImageWH));
        if (!tmp) {
            return false;
        }

        if (!skgpu::ganesh::DrawDDL(tmp, std::move(ddl))) {
            return false;
        }

        return this->checkImage(reporter, std::move(tmp));
    }

    size_t gpuSize(int wh) const {
        GrBackendFormat format = fDContext->defaultBackendFormat(kRGBA_8888_SkColorType,
                                                                 GrRenderable::kNo);

        return GrSurface::ComputeSize(format,
                                      {wh, wh},
                                      /*colorSamplesPerPixel=*/1,
                                      skgpu::Mipmapped::kNo,
                                      /*binSize=*/false);
    }

private:
    static GrSurfaceProxyView AccessCachedView(GrRecordingContext*,
                                               GrThreadSafeCache*,
                                               int wh,
                                               bool failLookup, bool failFillingIn, int id,
                                               Stats*);
    static GrSurfaceProxyView CreateViewOnCpu(GrRecordingContext*, int wh, Stats*);
    static bool FillInViewOnGpu(GrDirectContext*, int wh, Stats*,
                                const GrSurfaceProxyView& lazyView,
                                sk_sp<GrThreadSafeCache::Trampoline>);

    Stats fStats;
    GrDirectContext* fDContext = nullptr;
    GrThreadSafeCache::IsNewerBetter fIsNewerBetter;

    sk_sp<SkSurface> fDst;
    std::unique_ptr<GrDeferredDisplayListRecorder> fRecorder1;
    std::unique_ptr<GrDeferredDisplayListRecorder> fRecorder2;
};

class GrThreadSafeVertexTestOp : public GrDrawOp {
public:
    DEFINE_OP_CLASS_ID

    static GrOp::Owner Make(GrRecordingContext* rContext, TestHelper::Stats* stats,
                            int wh, int id, bool failLookup, bool failFillingIn,
                            GrThreadSafeCache::IsNewerBetter isNewerBetter) {

        return GrOp::Make<GrThreadSafeVertexTestOp>(
                rContext, rContext, stats, wh, id, failLookup, failFillingIn, isNewerBetter);
    }

    const GrThreadSafeCache::VertexData* vertexData() const { return fVertexData.get(); }

private:
    friend class GrOp; // for ctor

    GrThreadSafeVertexTestOp(GrRecordingContext* rContext, TestHelper::Stats* stats, int wh, int id,
                             bool failLookup, bool failFillingIn,
                             GrThreadSafeCache::IsNewerBetter isNewerBetter)
            : INHERITED(ClassID())
            , fStats(stats)
            , fWH(wh)
            , fID(id)
            , fFailFillingIn(failFillingIn)
            , fIsNewerBetter(isNewerBetter) {
        this->setBounds(SkRect::MakeIWH(fWH, fWH), HasAABloat::kNo, IsHairline::kNo);

        // Normally we wouldn't add a ref to the vertex data at this point. However, it is
        // needed in this unit test to get the ref counts on the uniquely keyed resources
        // to be as expected.
        this->findOrCreateVertices(rContext, failLookup, fFailFillingIn);
    }

    const char* name() const override { return "GrThreadSafeVertexTestOp"; }
    FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
    GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, GrClampType) override {
        return GrProcessorSet::EmptySetAnalysis();
    }

    GrProgramInfo* createProgramInfo(const GrCaps* caps,
                                     SkArenaAlloc* arena,
                                     const GrSurfaceProxyView& writeView,
                                     bool usesMSAASurface,
                                     GrAppliedClip&& appliedClip,
                                     const GrDstProxyView& dstProxyView,
                                     GrXferBarrierFlags renderPassXferBarriers,
                                     GrLoadOp colorLoadOp) const {
        using namespace GrDefaultGeoProcFactory;

        Color color({ 0.0f, 0.0f, 1.0f, 1.0f });

        auto gp = MakeForDeviceSpace(arena, color,
                                     Coverage::kSolid_Type,
                                     LocalCoords::kUnused_Type,
                                     SkMatrix::I());

        return sk_gpu_test::CreateProgramInfo(caps, arena, writeView, usesMSAASurface,
                                              std::move(appliedClip), dstProxyView,
                                              gp, SkBlendMode::kSrcOver,
                                              GrPrimitiveType::kTriangleStrip,
                                              renderPassXferBarriers, colorLoadOp);
    }

    GrProgramInfo* createProgramInfo(GrOpFlushState* flushState) const {
        return this->createProgramInfo(&flushState->caps(),
                                       flushState->allocator(),
                                       flushState->writeView(),
                                       flushState->usesMSAASurface(),
                                       flushState->detachAppliedClip(),
                                       flushState->dstProxyView(),
                                       flushState->renderPassBarriers(),
                                       flushState->colorLoadOp());
    }

    void findOrCreateVertices(GrRecordingContext* rContext, bool failLookup, bool failFillingIn) {

        if (!fVertexData) {
            auto threadSafeViewCache = rContext->priv().threadSafeCache();

            if (rContext->asDirectContext()) {
                // The vertex variant doesn't have a correlate to lazyProxies but increment this
                // here to make the unit tests happy.
                ++fStats->fNumLazyCreations;
            }

            skgpu::UniqueKey key;
            create_vert_key(&key, fWH, fID);

            // We can "fail the lookup" to simulate a threaded race condition
            auto [cachedVerts, data] = threadSafeViewCache->findVertsWithData(key);
            if (cachedVerts && !failLookup) {
                fVertexData = cachedVerts;
                ++fStats->fCacheHits;
                return;
            }

            ++fStats->fCacheMisses;
            if (!rContext->asDirectContext()) {
                ++fStats->fNumSWCreations;
            }

            constexpr size_t kVertSize = sizeof(SkPoint);
            SkPoint* verts = static_cast<SkPoint*>(sk_malloc_throw(4 * kVertSize));

            verts[0].set(10.0f, 10.0f);
            verts[1].set(fWH-10.0f, 10.0f);
            verts[2].set(10.0f, fWH-10.0f);
            verts[3].set(fWH-10.0f, fWH-10.0f);

            fVertexData = GrThreadSafeCache::MakeVertexData(verts, 4, kVertSize);

            auto [tmpV, tmpD] = threadSafeViewCache->addVertsWithData(key, fVertexData,
                                                                      fIsNewerBetter);
            if (tmpV != fVertexData) {
                // Someone beat us to creating the vertex data. Use that version.
                fVertexData = tmpV;
            }
        }

        if (auto dContext = rContext->asDirectContext(); dContext && !fVertexData->gpuBuffer()) {
            auto rp = dContext->priv().resourceProvider();

            if (!failFillingIn) {
                ++fStats->fNumHWCreations;

                sk_sp<GrGpuBuffer> tmp = rp->createBuffer(fVertexData->vertices(),
                                                          fVertexData->size(),
                                                          GrGpuBufferType::kVertex,
                                                          kStatic_GrAccessPattern);
                fVertexData->setGpuBuffer(std::move(tmp));
            }
        }
    }

    void onPrePrepare(GrRecordingContext* rContext,
                      const GrSurfaceProxyView& writeView,
                      GrAppliedClip* clip,
                      const GrDstProxyView& dstProxyView,
                      GrXferBarrierFlags renderPassXferBarriers,
                      GrLoadOp colorLoadOp) override {
        SkArenaAlloc* arena = rContext->priv().recordTimeAllocator();

        // DMSAA is not supported on DDL.
        bool usesMSAASurface = writeView.asRenderTargetProxy()->numSamples() > 1;

        // This is equivalent to a GrOpFlushState::detachAppliedClip
        GrAppliedClip appliedClip = clip ? std::move(*clip) : GrAppliedClip::Disabled();

        fProgramInfo = this->createProgramInfo(rContext->priv().caps(), arena, writeView,
                                               usesMSAASurface, std::move(appliedClip),
                                               dstProxyView, renderPassXferBarriers, colorLoadOp);

        rContext->priv().recordProgramInfo(fProgramInfo);

        // This is now a noop (bc it is always called in the ctor) but is where we would normally
        // create the vertices.
        this->findOrCreateVertices(rContext, false, fFailFillingIn);
    }

    void onPrepare(GrOpFlushState* flushState) override {
        auto dContext = flushState->gpu()->getContext();

        // This call site is not a noop bc this op could've been created on with DDL context
        // and, therefore, could be lacking a gpu-side buffer
        this->findOrCreateVertices(dContext, false, fFailFillingIn);
    }

    void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override {
        if (!fVertexData || !fVertexData->gpuBuffer()) {
            return;
        }

        if (!fProgramInfo) {
            fProgramInfo = this->createProgramInfo(flushState);
        }

        flushState->bindPipeline(*fProgramInfo, SkRect::MakeIWH(fWH, fWH));
        flushState->bindBuffers(nullptr, nullptr, fVertexData->refGpuBuffer());
        flushState->draw(4, 0);
    }

    TestHelper::Stats*               fStats;
    int                              fWH;
    int                              fID;
    bool                             fFailFillingIn;
    GrThreadSafeCache::IsNewerBetter fIsNewerBetter;

    sk_sp<GrThreadSafeCache::VertexData> fVertexData;
    GrProgramInfo*                   fProgramInfo = nullptr;

    using INHERITED = GrDrawOp;
};

void TestHelper::addVertAccess(SkCanvas* canvas,
                               int wh, int id,
                               bool failLookup, bool failFillingIn,
                               GrThreadSafeVertexTestOp** createdOp) {
    auto rContext = canvas->recordingContext();
    auto sdc = skgpu::ganesh::TopDeviceSurfaceDrawContext(canvas);

    GrOp::Owner op = GrThreadSafeVertexTestOp::Make(rContext, &fStats,
                                                    wh, id,
                                                    failLookup, failFillingIn,
                                                    fIsNewerBetter);
    if (createdOp) {
        *createdOp = (GrThreadSafeVertexTestOp*) op.get();
    }

    sdc->addDrawOp(std::move(op));
}

GrSurfaceProxyView TestHelper::CreateViewOnCpu(GrRecordingContext* rContext,
                                               int wh,
                                               Stats* stats) {
    GrProxyProvider* proxyProvider = rContext->priv().proxyProvider();

    sk_sp<GrTextureProxy> proxy = proxyProvider->createProxyFromBitmap(
            create_bitmap(wh), skgpu::Mipmapped::kNo, SkBackingFit::kExact, skgpu::Budgeted::kYes);
    if (!proxy) {
        return {};
    }

    skgpu::Swizzle swizzle = rContext->priv().caps()->getReadSwizzle(proxy->backendFormat(),
                                                                     GrColorType::kRGBA_8888);
    ++stats->fNumSWCreations;
    return {std::move(proxy), kImageOrigin, swizzle};
}

bool TestHelper::FillInViewOnGpu(GrDirectContext* dContext, int wh, Stats* stats,
                                 const GrSurfaceProxyView& lazyView,
                                 sk_sp<GrThreadSafeCache::Trampoline> trampoline) {
    std::unique_ptr<skgpu::ganesh::SurfaceDrawContext> sdc = new_SDC(dContext, wh);

    GrPaint paint;
    paint.setColor4f({0.0f, 0.0f, 1.0f, 1.0f});

    sdc->clear(SkPMColor4f{1.0f, 1.0f, 1.0f, 1.0f});
    sdc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
                  { 10, 10, wh-10.0f, wh-10.0f }, &GrStyle::SimpleFill());

    ++stats->fNumHWCreations;
    auto view = sdc->readSurfaceView();

    SkASSERT(view.swizzle() == lazyView.swizzle());
    SkASSERT(view.origin() == lazyView.origin());
    trampoline->fProxy = view.asTextureProxyRef();

    return true;
}

GrSurfaceProxyView TestHelper::AccessCachedView(GrRecordingContext* rContext,
                                                GrThreadSafeCache* threadSafeCache,
                                                int wh,
                                                bool failLookup, bool failFillingIn, int id,
                                                Stats* stats) {
    skgpu::UniqueKey key;
    create_view_key(&key, wh, id);

    if (GrDirectContext* dContext = rContext->asDirectContext()) {
        // The gpu thread gets priority over the recording threads. If the gpu thread is first,
        // it crams a lazy proxy into the cache and then fills it in later.
        auto [lazyView, trampoline] = GrThreadSafeCache::CreateLazyView(
            dContext, GrColorType::kRGBA_8888, {wh, wh}, kImageOrigin, SkBackingFit::kExact);
        ++stats->fNumLazyCreations;

        auto [view, data] = threadSafeCache->findOrAddWithData(key, lazyView);
        if (view != lazyView) {
            ++stats->fCacheHits;
            return view;
        } else if (id != kNoID) {
            // Make sure, in this case, that the customData stuck
            SkASSERT(data);
            SkDEBUGCODE(const int* cachedID = static_cast<const int*>(data->data());)
            SkASSERT(*cachedID == id);
        }

        ++stats->fCacheMisses;

        if (failFillingIn) {
            // Simulate something going horribly wrong at flush-time so no GrTexture is
            // available to fulfill the lazy proxy.
            return view;
        }

        if (!FillInViewOnGpu(dContext, wh, stats, lazyView, std::move(trampoline))) {
            // In this case something has gone disastrously wrong so set up to drop the draw
            // that needed this resource and reduce future pollution of the cache.
            threadSafeCache->remove(key);
            return {};
        }

        return view;
    } else {
        GrSurfaceProxyView view;

        // We can "fail the lookup" to simulate a threaded race condition
        if (view = threadSafeCache->find(key); !failLookup && view) {
            ++stats->fCacheHits;
            return view;
        }

        ++stats->fCacheMisses;

        view = CreateViewOnCpu(rContext, wh, stats);
        SkASSERT(view);

        auto [newView, data] = threadSafeCache->addWithData(key, view);
        if (view == newView && id != kNoID) {
            // Make sure, in this case, that the customData stuck
            SkASSERT(data);
            SkDEBUGCODE(const int* cachedID = static_cast<const int*>(data->data());)
            SkASSERT(*cachedID == id);
        }
        return newView;
    }
}

// Case 1: ensure two DDL recorders share the view/vertexData
static void test_1(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1));

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 2, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1);

    helper.checkImage(reporter, helper.snap1());
    helper.checkImage(reporter, helper.snap2());
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache1View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_1(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache1Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_1(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 2: ensure that, if the direct context version wins, its result is reused by the
//         DDL recorders
static void test_2(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, 1, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1));

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 2, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1));

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 3, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);

    helper.checkImage(reporter);
    helper.checkImage(reporter, helper.snap1());
    helper.checkImage(reporter, helper.snap2());
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache2View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_2(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache2Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_2(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 3: ensure that, if the cpu-version wins, its result is reused by the direct context
static void test_3(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1));

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, 2, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1);

    helper.checkImage(reporter);
    helper.checkImage(reporter, helper.snap1());
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache3View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_3(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache3Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_3(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 4: ensure that, if two DDL recorders get in a race, they still end up sharing a single
//         view/vertexData
static void test_4(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 1, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1));

    static const bool kFailLookup = true;
    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, 2, kFailLookup, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 2, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 2);

    helper.checkImage(reporter, helper.snap1());
    helper.checkImage(reporter, helper.snap2());
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 4.5: check that, if a live rendering and a DDL recording get into a race, the live
//           rendering takes precedence.
static void test_4_5(GrDirectContext* dContext, skiatest::Reporter* reporter,
                     TestHelper::addAccessFP addAccess,
                     TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, 1, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);

    static const bool kFailLookup = true;
    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, 2, kFailLookup, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 2, /*id*/ 1));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1);

    helper.checkImage(reporter);
    helper.checkImage(reporter, helper.snap1());
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_5View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4_5(ctxInfo.directContext(), reporter,
             &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_5Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4_5(ctxInfo.directContext(), reporter,
             &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 4.75: check that, if a live rendering fails to generate the content needed to instantiate
//            its lazy proxy, life goes on
static void test_4_75(GrDirectContext* dContext, skiatest::Reporter* reporter,
                      TestHelper::addAccessFP addAccess,
                      TestHelper::checkFP check) {

    TestHelper helper(dContext);

    static const bool kFailFillingIn = true;
    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, kFailFillingIn);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);

    dContext->flush();
    dContext->submit(GrSyncCpu::kYes);

    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 0, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_75View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4_75(ctxInfo.directContext(), reporter,
              &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache4_75Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_4_75(ctxInfo.directContext(), reporter,
              &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 5: ensure that expanding the map works (esp. wrt custom data)
static void test_5(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    auto threadSafeCache = helper.threadSafeCache();

    int size = 16;
    (helper.*addAccess)(helper.ddlCanvas1(), size, /*id*/ size, false, false);

    size_t initialSize = threadSafeCache->approxBytesUsedForHash();

    while (initialSize == threadSafeCache->approxBytesUsedForHash()) {
        size *= 2;
        (helper.*addAccess)(helper.ddlCanvas1(), size, /*id*/ size, false, false);
    }

    for (int i = 16; i <= size; i *= 2) {
        REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(),
                                                  /*wh*/ i,
                                                  /*hits*/ 0,
                                                  /*misses*/ threadSafeCache->numEntries(),
                                                  /*refs*/ 1,
                                                  /*id*/ i));
    }
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache5View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_5(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache5Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_5(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 6: Check on dropping refs. In particular, that the cache has its own ref to keep
//         the backing resource alive and locked.
static void test_6(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    ddl1 = nullptr;
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 1, kNoID));

    ddl2 = nullptr;
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 0, kNoID));

    // The cache still has its ref
    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 0, kNoID));
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache6View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_6(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache6Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_6(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 7: Check that invoking dropAllRefs and dropUniqueRefs directly works as expected; i.e.,
//         dropAllRefs removes everything while dropUniqueRefs is more measured.
static void test_7(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);

    helper.threadSafeCache()->dropUniqueRefs(nullptr);
    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);

    ddl1 = nullptr;

    helper.threadSafeCache()->dropUniqueRefs(nullptr);
    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, (helper.*check)(nullptr, 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));

    helper.threadSafeCache()->dropAllRefs();
    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);

    ddl2 = nullptr;
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache7View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_7(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache7Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_7(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 8: This checks that GrContext::abandonContext works as expected wrt the thread
//         safe cache. This simulates the case where we have one DDL that has finished
//         recording but one still recording when the abandonContext fires.
static void test_8(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID));

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);

    dContext->abandonContext(); // This should exercise dropAllRefs

    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);

    ddl1 = nullptr;
    ddl2 = nullptr;
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache8View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_8(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache8Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_8(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 9: This checks that GrContext::releaseResourcesAndAbandonContext works as expected wrt
//         the thread safe cache. This simulates the case where we have one DDL that has finished
//         recording but one still recording when the releaseResourcesAndAbandonContext fires.
static void test_9(GrDirectContext* dContext, skiatest::Reporter* reporter,
                   TestHelper::addAccessFP addAccess,
                   TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID));

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 2, /*misses*/ 1, /*refs*/ 3, kNoID));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumLazyCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);

    dContext->releaseResourcesAndAbandonContext(); // This should hit dropAllRefs

    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);

    ddl1 = nullptr;
    ddl2 = nullptr;
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache9View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_9(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache9Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_9(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 10: This checks that the GrContext::purgeUnlockedResources(size_t) variant works as
//          expected wrt the thread safe cache. It, in particular, tests out the MRU behavior
//          of the shared cache.
static void test_10(GrDirectContext* dContext, skiatest::Reporter* reporter,
                    TestHelper::addAccessFP addAccess,
                    TestHelper::checkFP check) {

    if (GrBackendApi::kOpenGL != dContext->backend()) {
        // The lower-level backends have too much going on for the following simple purging
        // test to work
        return;
    }

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID));

    (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), 2*kImageWH,
                                              /*hits*/ 2, /*misses*/ 2, /*refs*/ 2, kNoID));

    dContext->flush();
    dContext->submit(GrSyncCpu::kYes);

    // This should clear out everything but the textures locked in the thread-safe cache
    dContext->purgeUnlockedResources(GrPurgeResourceOptions::kAllResources);

    ddl1 = nullptr;
    ddl2 = nullptr;

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID));
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID));

    // Regardless of which image is MRU, this should force the other out
    size_t desiredBytes = helper.gpuSize(2*kImageWH) + helper.gpuSize(kImageWH)/2;

    auto cache = dContext->priv().getResourceCache();
    size_t currentBytes = cache->getResourceBytes();

    SkASSERT(currentBytes >= desiredBytes);
    size_t amountToPurge = currentBytes - desiredBytes;

    // The 2*kImageWH texture should be MRU.
    dContext->purgeUnlockedResources(amountToPurge, true);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 2, /*misses*/ 2, /*refs*/ 0, kNoID));
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache10View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_10(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

// To enable test_10 with verts would require a bit more work, namely:
//    have a different # of verts based on size
//    also pass in a gpuSize function to 'test_10'
// DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache10Verts, reporter, ctxInfo) {
//    test_10(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess,
//    &TestHelper::checkVert);
//}

// Case 11: This checks that scratch-only variant of GrContext::purgeUnlockedResources works as
//          expected wrt the thread safe cache. In particular, that when 'scratchResourcesOnly'
//          is true, the call has no effect on the cache.
static void test_11(GrDirectContext* dContext, skiatest::Reporter* reporter,
                    TestHelper::addAccessFP addAccess,
                    TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));

    (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));

    dContext->flush();
    dContext->submit(GrSyncCpu::kYes);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID));
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID));

    // This shouldn't remove anything from the cache
    dContext->purgeUnlockedResources(GrPurgeResourceOptions::kScratchResourcesOnly);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);

    dContext->purgeUnlockedResources(GrPurgeResourceOptions::kAllResources);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache11View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_11(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache11Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_11(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 12: Test out purges caused by resetting the cache budget to 0. Note that, due to
//          the how the cache operates (i.e., not directly driven by ref/unrefs) there
//          needs to be an explicit kick to purge the cache.
static void test_12(GrDirectContext* dContext, skiatest::Reporter* reporter,
                    TestHelper::addAccessFP addAccess,
                    TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.liveCanvas(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));
    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 1, /*refs*/ 2, kNoID));

    (helper.*addAccess)(helper.liveCanvas(), 2*kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID));

    dContext->flush();
    dContext->submit(GrSyncCpu::kYes);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), kImageWH,
                                              /*hits*/ 1, /*misses*/ 2, /*refs*/ 1, kNoID));
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 1, /*misses*/ 2, /*refs*/ 0, kNoID));

    dContext->setResourceCacheLimit(0);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    ddl1 = nullptr;

    // Explicitly kick off the purge - it won't happen automatically on unref
    dContext->performDeferredCleanup(std::chrono::milliseconds(0));

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache12View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_12(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache12Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_12(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 13: Test out the 'msNotUsed' parameter to GrContext::performDeferredCleanup.
static void test_13(GrDirectContext* dContext, skiatest::Reporter* reporter,
                    TestHelper::addAccessFP addAccess,
                    TestHelper::checkFP check) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();

    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    auto firstTime = skgpu::StdSteadyClock::now();
    std::this_thread::sleep_for(std::chrono::milliseconds(5));

    (helper.*addAccess)(helper.ddlCanvas2(), 2*kImageWH, kNoID, false, false);

    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();

    ddl1 = nullptr;
    ddl2 = nullptr;

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);

    auto secondTime = skgpu::StdSteadyClock::now();

    auto msecs = std::chrono::duration_cast<std::chrono::milliseconds>(secondTime - firstTime);
    dContext->performDeferredCleanup(msecs);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.liveCanvas(), 2*kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 0, kNoID));
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache13View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_13(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache13Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_13(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert);
}

// Case 14: Test out mixing & matching view & vertex data w/ recycling of the cache entries to
//          wring out the anonymous union code. This is mainly for the MSAN bot's consumption.
DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache14,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    constexpr int kBestPrimeNumber = 73; // palindromic in binary
    SkRandom rand(kBestPrimeNumber);

    TestHelper helper(ctxInfo.directContext());

    for (int i = 0; i < 2; ++i) {
        SkCanvas* ddlCanvas = (!i) ? helper.ddlCanvas1() : helper.ddlCanvas2();

        for (int j = 0; j < 10; ++j) {
            int numResources = 10*i + j + 1;
            int wh = numResources;

            if (rand.nextBool()) {
                helper.addViewAccess(ddlCanvas, wh, kNoID, false, false);
                REPORTER_ASSERT(reporter, helper.checkView(ddlCanvas, wh,
                                                           /*hits*/ 0, /*misses*/ numResources,
                                                           /*refs*/ 1, kNoID));
            } else {
                helper.addVertAccess(ddlCanvas, wh, kNoID, false, false);
                REPORTER_ASSERT(reporter, helper.checkVert(ddlCanvas, wh,
                                                           /*hits*/ 0, /*misses*/ numResources,
                                                           /*refs*/ 1, kNoID));
            }
        }

        if (!i) {
            // Drop all the accumulated resources from the thread-safe cache
            helper.snap1();
            ctxInfo.directContext()->purgeUnlockedResources(GrPurgeResourceOptions::kAllResources);
        }
    }
}

// Case 15: Test out posting invalidation messages that involve the thread safe cache
static void test_15(GrDirectContext* dContext, skiatest::Reporter* reporter,
                    TestHelper::addAccessFP addAccess,
                    TestHelper::checkFP check,
                    void (*create_key)(skgpu::UniqueKey*, int wh, int id)) {

    TestHelper helper(dContext);

    (helper.*addAccess)(helper.ddlCanvas1(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas1(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    skgpu::UniqueKey key;
    (*create_key)(&key, kImageWH, kNoID);

    skgpu::UniqueKeyInvalidatedMessage msg(key, dContext->priv().contextID(),
                                      /* inThreadSafeCache */ true);

    SkMessageBus<skgpu::UniqueKeyInvalidatedMessage, uint32_t>::Post(msg);

    // This purge call is needed to process the invalidation messages
    dContext->purgeUnlockedResources(GrPurgeResourceOptions::kScratchResourcesOnly);

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);

    (helper.*addAccess)(helper.ddlCanvas2(), kImageWH, kNoID, false, false);
    REPORTER_ASSERT(reporter, (helper.*check)(helper.ddlCanvas2(), kImageWH,
                                              /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();

    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);

    helper.checkImage(reporter, std::move(ddl1));
    helper.checkImage(reporter, std::move(ddl2));
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache15View,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_15(ctxInfo.directContext(), reporter, &TestHelper::addViewAccess, &TestHelper::checkView,
            create_view_key);
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache15Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    test_15(ctxInfo.directContext(), reporter, &TestHelper::addVertAccess, &TestHelper::checkVert,
            create_vert_key);
}

// Case 16: Test out pre-emption of an existing vertex-data cache entry. This test simulates
//          the case where there is a race to create vertex data. However, the second one
//          to finish is better and usurps the first's position in the cache.
//
//          This capability isn't available for views.

static bool newer_is_always_better(SkData* /* incumbent */, SkData* /* challenger */) {
    return true;
}

DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(GrThreadSafeCache16Verts,
                                       reporter,
                                       ctxInfo,
                                       CtsEnforcement::kNever) {
    skgpu::UniqueKey key;
    create_vert_key(&key, kImageWH, kNoID);

    TestHelper helper(ctxInfo.directContext(), newer_is_always_better);

    GrThreadSafeVertexTestOp* op1 = nullptr, *op2 = nullptr;

    helper.addVertAccess(helper.ddlCanvas1(), kImageWH, kNoID, false, false, &op1);
    REPORTER_ASSERT(reporter, helper.checkVert(helper.ddlCanvas1(), kImageWH,
                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl1 = helper.snap1();

    {
        REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
        auto [vertexData, xtraData] = helper.threadSafeCache()->findVertsWithData(key);
        REPORTER_ASSERT(reporter, vertexData.get() == op1->vertexData());
    }

    helper.addVertAccess(helper.ddlCanvas2(), kImageWH, kNoID, /* failLookup */ true, false, &op2);
    REPORTER_ASSERT(reporter, helper.checkVert(helper.ddlCanvas2(), kImageWH,
                                               /*hits*/ 0, /*misses*/ 2, /*refs*/ 1, kNoID));
    sk_sp<GrDeferredDisplayList> ddl2 = helper.snap2();

    REPORTER_ASSERT(reporter, op1->vertexData() != op2->vertexData());

    {
        REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
        auto [vertexData, xtraData] = helper.threadSafeCache()->findVertsWithData(key);
        REPORTER_ASSERT(reporter, vertexData.get() == op2->vertexData());
    }

    helper.checkImage(reporter, std::move(ddl1));
    helper.checkImage(reporter, std::move(ddl2));
}
