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

#include "bench/Benchmark.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkSurface.h"
#include "include/gpu/GrDirectContext.h"
#include "src/gpu/GrContextPriv.h"
#include "tools/ToolUtils.h"


#include <utility>

/** These benchmarks were designed to measure changes to GrResourceCache's replacement policy */

//////////////////////////////////////////////////////////////////////////////

// The width/height of the images to draw. The small size underestimates the value of a good
// replacement strategy since the texture uploads are quite small. However, the effects are still
// significant and this lets the benchmarks complete a lot faster, especially on mobile.
static constexpr int kS = 25;

static void make_images(sk_sp<SkImage> imgs[], int cnt) {
    for (int i = 0; i < cnt; ++i) {
        SkBitmap bmp =
                ToolUtils::create_checkerboard_bitmap(kS, kS, SK_ColorBLACK, SK_ColorCYAN, 10);
        imgs[i] = SkImage::MakeFromBitmap(bmp);
    }
}

static void draw_image(SkCanvas* canvas, SkImage* img) {
    // Make the paint transparent to avoid any issues of deferred tiler blending
    // optmizations
    SkPaint paint;
    paint.setAlpha(0x10);
    canvas->drawImage(img, 0, 0, &paint);
}

void set_cache_budget(SkCanvas* canvas, int approxImagesInBudget) {
    // This is inexact but we attempt to figure out a baseline number of resources GrContext needs
    // to render an SkImage and add one additional resource for each image we'd like to fit.
    auto context =  canvas->recordingContext()->asDirectContext();
    SkASSERT(context);
    context->flushAndSubmit();
    context->priv().testingOnly_purgeAllUnlockedResources();
    sk_sp<SkImage> image;
    make_images(&image, 1);
    draw_image(canvas, image.get());
    context->flushAndSubmit();
    int baselineCount;
    context->getResourceCacheUsage(&baselineCount, nullptr);
    baselineCount -= 1; // for the image's textures.
    context->setResourceCacheLimits(baselineCount + approxImagesInBudget, 1 << 30);
    context->priv().testingOnly_purgeAllUnlockedResources();
}

//////////////////////////////////////////////////////////////////////////////

/**
 * Tests repeatedly drawing the same set of images in each frame. Different instances of the bench
 * run with different cache sizes and either repeat the image order each frame or use a random
 * order. Every variation of this bench draws the same image set, only the budget and order of
 * images differs. Since the total fill is the same they can be cross-compared.
 */
class ImageCacheBudgetBench : public Benchmark {
public:
    /** budgetSize is the number of images that can fit in the cache. 100 images will be drawn. */
    ImageCacheBudgetBench(int budgetSize, bool shuffle)
            : fBudgetSize(budgetSize)
            , fShuffle(shuffle)
            , fIndices(nullptr) {
        float imagesOverBudget = float(kImagesToDraw) / budgetSize;
        // Make the benchmark name contain the percentage of the budget that is used in each
        // simulated frame.
        fName.printf("image_cache_budget_%.0f%s", imagesOverBudget * 100,
                     (shuffle ? "_shuffle" : ""));
    }

    bool isSuitableFor(Backend backend) override { return kGPU_Backend == backend; }

protected:
    const char* onGetName() override {
        return fName.c_str();
    }

    void onPerCanvasPreDraw(SkCanvas* canvas) override {
        auto context = canvas->recordingContext()->asDirectContext();
        SkASSERT(context);
        fOldBytes = context->getResourceCacheLimit();
        set_cache_budget(canvas, fBudgetSize);
        make_images(fImages, kImagesToDraw);
        if (fShuffle) {
            SkRandom random;
            fIndices.reset(new int[kSimulatedFrames * kImagesToDraw]);
            for (int frame = 0; frame < kSimulatedFrames; ++frame) {
                int* base = fIndices.get() + frame * kImagesToDraw;
                for (int i = 0; i < kImagesToDraw; ++i) {
                    base[i] = i;
                }
                for (int i = 0; i < kImagesToDraw - 1; ++i) {
                    int other = random.nextULessThan(kImagesToDraw - i) + i;
                    using std::swap;
                    swap(base[i], base[other]);
                }
            }
        }
    }

    void onPerCanvasPostDraw(SkCanvas* canvas) override {
        auto context =  canvas->recordingContext()->asDirectContext();
        SkASSERT(context);
        context->setResourceCacheLimit(fOldBytes);
        for (int i = 0; i < kImagesToDraw; ++i) {
            fImages[i].reset();
        }
        fIndices.reset(nullptr);
    }

    void onDraw(int loops, SkCanvas* canvas) override {
        for (int i = 0; i < loops; ++i) {
            for (int frame = 0; frame < kSimulatedFrames; ++frame) {
                for (int j = 0; j < kImagesToDraw; ++j) {
                    int idx;
                    if (fShuffle) {
                        idx = fIndices[frame * kImagesToDraw + j];
                    } else {
                        idx = j;
                    }
                    draw_image(canvas, fImages[idx].get());
                }
                // Simulate a frame boundary by flushing. This should notify GrResourceCache.
                canvas->flush();
           }
        }
    }

private:
    static constexpr int kImagesToDraw = 100;
    static constexpr int kSimulatedFrames = 5;

    int                         fBudgetSize;
    bool                        fShuffle;
    SkString                    fName;
    sk_sp<SkImage>              fImages[kImagesToDraw];
    std::unique_ptr<int[]>      fIndices;
    size_t                      fOldBytes;

    typedef Benchmark INHERITED;
};

DEF_BENCH( return new ImageCacheBudgetBench(105, false); )

DEF_BENCH( return new ImageCacheBudgetBench(90, false); )

DEF_BENCH( return new ImageCacheBudgetBench(80, false); )

DEF_BENCH( return new ImageCacheBudgetBench(50, false); )

DEF_BENCH( return new ImageCacheBudgetBench(105, true); )

DEF_BENCH( return new ImageCacheBudgetBench(90, true); )

DEF_BENCH( return new ImageCacheBudgetBench(80, true); )

DEF_BENCH( return new ImageCacheBudgetBench(50, true); )

//////////////////////////////////////////////////////////////////////////////

/**
 * Similar to above but changes between being over and under budget by varying the number of images
 * rendered. This is not directly comparable to the non-dynamic benchmarks.
 */
class ImageCacheBudgetDynamicBench : public Benchmark {
public:
    enum class Mode {
        // Increase from min to max images drawn gradually over simulated frames and then back.
        kPingPong,
        // Alternate between under and over budget every other simulated frame.
        kFlipFlop
    };

    ImageCacheBudgetDynamicBench(Mode mode) : fMode(mode) {}

    bool isSuitableFor(Backend backend) override { return kGPU_Backend == backend; }

protected:
    const char* onGetName() override {
        switch (fMode) {
            case Mode::kPingPong:
                return "image_cache_budget_dynamic_ping_pong";
            case Mode::kFlipFlop:
                return "image_cache_budget_dynamic_flip_flop";
        }
        return "";
    }

    void onPerCanvasPreDraw(SkCanvas* canvas) override {
        auto context = canvas->recordingContext()->asDirectContext();
        SkASSERT(context);
        context->getResourceCacheLimits(&fOldCount, &fOldBytes);
        make_images(fImages, kMaxImagesToDraw);
        set_cache_budget(canvas, kImagesInBudget);
    }

    void onPerCanvasPostDraw(SkCanvas* canvas) override {
        auto context = canvas->recordingContext()->asDirectContext();
        SkASSERT(context);
        context->setResourceCacheLimits(fOldCount, fOldBytes);
        for (int i = 0; i < kMaxImagesToDraw; ++i) {
            fImages[i].reset();
        }
    }

    void onDraw(int loops, SkCanvas* canvas) override {
        int delta = 0;
        switch (fMode) {
            case Mode::kPingPong:
                delta = 1;
                break;
            case Mode::kFlipFlop:
                delta = kMaxImagesToDraw - kMinImagesToDraw;
                break;
        }
        for (int i = 0; i < loops; ++i) {
            int imgsToDraw = kMinImagesToDraw;
            for (int frame = 0; frame < kSimulatedFrames; ++frame) {
                for (int j = 0; j < imgsToDraw; ++j) {
                    draw_image(canvas, fImages[j].get());
                }
                imgsToDraw += delta;
                if (imgsToDraw > kMaxImagesToDraw || imgsToDraw < kMinImagesToDraw) {
                    delta = -delta;
                    imgsToDraw += 2 * delta;
                }
                // Simulate a frame boundary by flushing. This should notify GrResourceCache.
                canvas->flush();
            }
        }
    }

private:
    static constexpr int kImagesInBudget  = 25;
    static constexpr int kMinImagesToDraw = 15;
    static constexpr int kMaxImagesToDraw = 35;
    static constexpr int kSimulatedFrames = 80;

    Mode                        fMode;
    sk_sp<SkImage>              fImages[kMaxImagesToDraw];
    size_t                      fOldBytes;
    int                         fOldCount;

    typedef Benchmark INHERITED;
};

DEF_BENCH( return new ImageCacheBudgetDynamicBench(ImageCacheBudgetDynamicBench::Mode::kPingPong); )
DEF_BENCH( return new ImageCacheBudgetDynamicBench(ImageCacheBudgetDynamicBench::Mode::kFlipFlop); )
