Add a fuzzer for DDL threading

This helps us find any issues with the promise image sharing.

Bug: skia:10286
Change-Id: I393655c2de76f896d9f376765894f84c015b2760
Cq-Include-Trybots: luci.skia.skia.primary:Fuzz-Debian10-Clang
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/374317
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Adlai Holler <adlai@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index b9e4084..78cb5dc 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -2276,6 +2276,7 @@
       "fuzz/FuzzCommon.cpp",
       "fuzz/FuzzCommon.h",
       "fuzz/FuzzCreateDDL.cpp",
+      "fuzz/FuzzDDLThreading.cpp",
       "fuzz/FuzzDrawFunctions.cpp",
       "fuzz/FuzzEncoders.cpp",
       "fuzz/FuzzGradients.cpp",
diff --git a/fuzz/FuzzDDLThreading.cpp b/fuzz/FuzzDDLThreading.cpp
new file mode 100644
index 0000000..b8caa85
--- /dev/null
+++ b/fuzz/FuzzDDLThreading.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "fuzz/Fuzz.h"
+#include "fuzz/FuzzCommon.h"
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkDeferredDisplayList.h"
+#include "include/core/SkDeferredDisplayListRecorder.h"
+#include "include/core/SkExecutor.h"
+#include "include/core/SkPromiseImageTexture.h"
+#include "include/core/SkSize.h"
+#include "include/core/SkSurface.h"
+#include "include/gpu/GrDirectContext.h"
+#include "include/private/SkDeque.h"
+#include "include/private/SkMutex.h"
+#include "include/private/SkNoncopyable.h"
+#include "include/private/SkTemplates.h"
+#include "include/private/SkThreadID.h"
+#include "src/core/SkTaskGroup.h"
+#include "src/image/SkImage_Gpu.h"
+#include "tools/gpu/GrContextFactory.h"
+
+#include <atomic>
+#include <memory>
+#include <queue>
+
+using ContextType = sk_gpu_test::GrContextFactory::ContextType;
+
+// be careful: `foo(make_fuzz_t<T>(f), make_fuzz_t<U>(f))` is undefined.
+// In fact, all make_fuzz_foo() functions have this potential problem.
+// Use sequence points!
+template <typename T>
+inline T make_fuzz_t(Fuzz* fuzz) {
+    T t;
+    fuzz->next(&t);
+    return t;
+}
+
+class DDLFuzzer;
+
+// This class stores the state of a given promise image owned by the fuzzer. It acts as the
+// context for the callback procs of the promise image.
+class PromiseImageInfo : public SkNVRefCnt<PromiseImageInfo>, SkNoncopyable {
+public:
+    enum class State : int {
+        kInitial,
+        kTriedToFulfill,
+        kDone
+    };
+    ~PromiseImageInfo() {
+        // If we hit this, then the image or the texture will outlive this object which is bad.
+        SkASSERT_RELEASE(fImage->unique());
+        SkASSERT_RELEASE(!fTexture || fTexture->unique());
+        fImage.reset();
+        fTexture.reset();
+        State s = fState;
+        SkASSERT_RELEASE(s == State::kDone);
+    }
+    DDLFuzzer* fFuzzer = nullptr;
+    sk_sp<SkImage> fImage;
+    // At the moment, the atomicity of this isn't used because all our promise image callbacks
+    // happen on the same thread. See the TODO below about them unreffing them off the GPU thread.
+    std::atomic<State> fState{State::kInitial};
+    sk_sp<SkPromiseImageTexture> fTexture;
+};
+
+static constexpr int kPromiseImageCount = 8;
+static constexpr SkISize kPromiseImageSize{16, 16};
+static constexpr int kPromiseImagesPerDDL = 4;
+static constexpr int kRecordingThreadCount = 4;
+static constexpr int kIterationCount = 10000;
+
+// A one-shot runner object for fuzzing our DDL threading. It creates an array of promise images,
+// and concurrently records DDLs that reference them, playing each DDL back on the GPU thread.
+// The backing textures for promise images may be recycled into a pool, or not, for each case
+// as determined by the fuzzing data.
+class DDLFuzzer : SkNoncopyable {
+public:
+    DDLFuzzer(Fuzz*, ContextType);
+    void run();
+
+    sk_sp<SkPromiseImageTexture> fulfillPromiseImage(PromiseImageInfo&);
+    void releasePromiseImage(PromiseImageInfo&);
+private:
+    void initPromiseImage(int index);
+    void recordAndPlayDDL();
+    bool isOnGPUThread() const { return SkGetThreadID() == fGpuThread; }
+    bool isOnMainThread() const { return SkGetThreadID() == fMainThread; }
+
+    Fuzz* fFuzz = nullptr;
+    GrDirectContext* fContext = nullptr;
+    SkAutoTArray<PromiseImageInfo> fPromiseImages{kPromiseImageCount};
+    sk_sp<SkSurface> fSurface;
+    SkSurfaceCharacterization fSurfaceCharacterization;
+    std::unique_ptr<SkExecutor> fGpuExecutor = SkExecutor::MakeFIFOThreadPool(1, false);
+    std::unique_ptr<SkExecutor> fRecordingExecutor =
+        SkExecutor::MakeFIFOThreadPool(kRecordingThreadCount, false);
+    SkTaskGroup fGpuTaskGroup{*fGpuExecutor};
+    SkTaskGroup fRecordingTaskGroup{*fRecordingExecutor};
+    SkThreadID fGpuThread = kIllegalThreadID;
+    SkThreadID fMainThread = SkGetThreadID();
+    std::queue<sk_sp<SkPromiseImageTexture>> fReusableTextures;
+    sk_gpu_test::GrContextFactory fContextFactory;
+};
+
+DDLFuzzer::DDLFuzzer(Fuzz* fuzz, ContextType contextType) : fFuzz(fuzz) {
+    sk_gpu_test::ContextInfo ctxInfo = fContextFactory.getContextInfo(contextType);
+    sk_gpu_test::TestContext* testCtx = ctxInfo.testContext();
+    fContext = ctxInfo.directContext();
+    SkISize canvasSize = kPromiseImageSize;
+    canvasSize.fWidth *= kPromiseImagesPerDDL;
+    SkImageInfo ii = SkImageInfo::Make(canvasSize, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
+    fSurface = SkSurface::MakeRenderTarget(fContext, SkBudgeted::kNo, ii);
+    SkAssertResult(fSurface->characterize(&fSurfaceCharacterization));
+
+    testCtx->makeNotCurrent();
+    fGpuTaskGroup.add([&]{
+        testCtx->makeCurrent();
+        fGpuThread = SkGetThreadID();
+    });
+    fGpuTaskGroup.wait();
+    for (int i = 0; i < kPromiseImageCount; ++i) {
+        this->initPromiseImage(i);
+    }
+}
+
+sk_sp<SkPromiseImageTexture> DDLFuzzer::fulfillPromiseImage(PromiseImageInfo& promiseImage) {
+    using State = PromiseImageInfo::State;
+    if (!this->isOnGPUThread()) {
+        fFuzz->signalBug();
+    }
+    bool success = make_fuzz_t<bool>(fFuzz);
+    State prior = promiseImage.fState.exchange(State::kTriedToFulfill, std::memory_order_relaxed);
+    if (prior != State::kInitial || promiseImage.fTexture != nullptr) {
+        fFuzz->signalBug();
+    }
+    if (!success) {
+        return nullptr;
+    }
+
+    // Try reusing an existing texture if we can and if the fuzzer wills it.
+    if (!fReusableTextures.empty() && make_fuzz_t<bool>(fFuzz)) {
+        promiseImage.fTexture = std::move(fReusableTextures.front());
+        fReusableTextures.pop();
+        return promiseImage.fTexture;
+    }
+
+    bool finishedBECreate = false;
+    auto markFinished = [](void* context) {
+        *(bool*)context = true;
+    };
+
+    GrBackendTexture backendTex = fContext->createBackendTexture(kPromiseImageSize.width(),
+                                                                 kPromiseImageSize.height(),
+                                                                 kRGBA_8888_SkColorType,
+                                                                 SkColors::kRed,
+                                                                 GrMipMapped::kNo,
+                                                                 GrRenderable::kYes,
+                                                                 GrProtected::kNo,
+                                                                 markFinished,
+                                                                 &finishedBECreate);
+    SkASSERT_RELEASE(backendTex.isValid());
+    while (!finishedBECreate) {
+        fContext->checkAsyncWorkCompletion();
+    }
+
+    promiseImage.fTexture = SkPromiseImageTexture::Make(backendTex);
+
+    return promiseImage.fTexture;
+}
+
+void DDLFuzzer::releasePromiseImage(PromiseImageInfo& promiseImage) {
+    using State = PromiseImageInfo::State;
+    // TODO: This requirement will go away when we unref promise images off the GPU thread.
+    if (!this->isOnGPUThread()) {
+        fFuzz->signalBug();
+    }
+    State old = promiseImage.fState.exchange(State::kInitial, std::memory_order_relaxed);
+    if (old != State::kTriedToFulfill) {
+        fFuzz->signalBug();
+    }
+
+    // If we failed to fulfill, then nothing to be done.
+    if (!promiseImage.fTexture) {
+        return;
+    }
+
+    bool reuse = make_fuzz_t<bool>(fFuzz);
+    if (reuse) {
+        fReusableTextures.push(std::move(promiseImage.fTexture));
+    } else {
+        fContext->deleteBackendTexture(promiseImage.fTexture->backendTexture());
+    }
+    promiseImage.fTexture = nullptr;
+}
+
+static sk_sp<SkPromiseImageTexture> fuzz_promise_image_fulfill(void* ctxIn) {
+    PromiseImageInfo& fuzzPromiseImage = *(PromiseImageInfo*)ctxIn;
+    return fuzzPromiseImage.fFuzzer->fulfillPromiseImage(fuzzPromiseImage);
+}
+
+static void fuzz_promise_image_release(void* ctxIn) {
+    PromiseImageInfo& fuzzPromiseImage = *(PromiseImageInfo*)ctxIn;
+    fuzzPromiseImage.fFuzzer->releasePromiseImage(fuzzPromiseImage);
+}
+
+void DDLFuzzer::initPromiseImage(int index) {
+    PromiseImageInfo& promiseImage = fPromiseImages[index];
+    promiseImage.fFuzzer = this;
+    GrBackendFormat backendFmt = fContext->defaultBackendFormat(kRGBA_8888_SkColorType,
+                                                                GrRenderable::kYes);
+    promiseImage.fImage = SkImage::MakePromiseTexture(fContext->threadSafeProxy(),
+                                                      backendFmt,
+                                                      kPromiseImageSize,
+                                                      GrMipMapped::kNo,
+                                                      kTopLeft_GrSurfaceOrigin,
+                                                      kRGBA_8888_SkColorType,
+                                                      kUnpremul_SkAlphaType,
+                                                      SkColorSpace::MakeSRGB(),
+                                                      &fuzz_promise_image_fulfill,
+                                                      &fuzz_promise_image_release,
+                                                      &promiseImage);
+}
+
+void DDLFuzzer::recordAndPlayDDL() {
+    SkASSERT(!this->isOnGPUThread() && !this->isOnMainThread());
+    SkDeferredDisplayListRecorder recorder(fSurfaceCharacterization);
+    SkCanvas* canvas = recorder.getCanvas();
+    // Draw promise images in a strip
+    for (int i = 0; i < kPromiseImagesPerDDL; i++) {
+        int xOffset = i * kPromiseImageSize.width();
+        int j;
+        // Pick random promise images to draw.
+        fFuzz->nextRange(&j, 0, kPromiseImageCount - 1);
+        canvas->drawImage(fPromiseImages[j].fImage, xOffset, 0);
+    }
+    sk_sp<SkDeferredDisplayList> ddl = recorder.detach();
+    fGpuTaskGroup.add([=, ddl{std::move(ddl)}]{
+        bool success = fSurface->draw(std::move(ddl));
+        if (!success) {
+            fFuzz->signalBug();
+        }
+    });
+}
+
+void DDLFuzzer::run() {
+    fRecordingTaskGroup.batch(kIterationCount, [=](int i) {
+        this->recordAndPlayDDL();
+    });
+    fRecordingTaskGroup.wait();
+    fGpuTaskGroup.add([=] {
+        while (!fReusableTextures.empty()) {
+            sk_sp<SkPromiseImageTexture> gpuTexture = std::move(fReusableTextures.front());
+            fContext->deleteBackendTexture(gpuTexture->backendTexture());
+            fReusableTextures.pop();
+        }
+        fContextFactory.destroyContexts();
+        // TODO: Release promise images not on the GPU thread.
+        fPromiseImages.reset(0);
+    });
+    fGpuTaskGroup.wait();
+}
+
+DEF_FUZZ(DDLThreadingGL, fuzz) {
+    DDLFuzzer(fuzz, ContextType::kGL_ContextType).run();
+}