/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "pipeline/skia/SkiaPipeline.h"

#include <SkCanvas.h>
#include <SkColor.h>
#include <SkColorSpace.h>
#include <SkData.h>
#include <SkImage.h>
#include <SkImageAndroid.h>
#include <SkImageInfo.h>
#include <SkMatrix.h>
#include <SkOverdrawCanvas.h>
#include <SkOverdrawColorFilter.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
#include <SkRect.h>
#include <SkRefCnt.h>
#include <SkSerialProcs.h>
#include <SkStream.h>
#include <SkString.h>
#include <SkTypeface.h>
#include <android-base/properties.h>
#include <gui/TraceUtils.h>
#include <include/android/SkSurfaceAndroid.h>
#include <include/docs/SkMultiPictureDocument.h>
#include <include/encode/SkPngEncoder.h>
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
#include <unistd.h>

#include <sstream>

#include "LightingInfo.h"
#include "VectorDrawable.h"
#include "include/gpu/GpuTypes.h"  // from Skia
#include "thread/CommonPool.h"
#include "tools/SkSharingProc.h"
#include "utils/Color.h"
#include "utils/String8.h"

using namespace android::uirenderer::renderthread;

namespace android {
namespace uirenderer {
namespace skiapipeline {

SkiaPipeline::SkiaPipeline(RenderThread& thread) : mRenderThread(thread) {
    setSurfaceColorProperties(mColorMode);
}

SkiaPipeline::~SkiaPipeline() {}

void SkiaPipeline::onDestroyHardwareResources() {
    unpinImages();
    mRenderThread.cacheManager().trimStaleResources();
}

void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry,
                                LayerUpdateQueue* layerUpdateQueue, bool opaque,
                                const LightInfo& lightInfo) {
    LightingInfo::updateLighting(lightGeometry, lightInfo);
    ATRACE_NAME("draw layers");
    renderLayersImpl(*layerUpdateQueue, opaque);
    layerUpdateQueue->clear();
}

bool SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) {
    SkASSERT(layerNode->getLayerSurface());
    SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl();
    if (!displayList || displayList->isEmpty()) {
        ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
        return false;
    }

    SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();

    int saveCount = layerCanvas->save();
    SkASSERT(saveCount == 1);

    layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());

    // TODO: put localized light center calculation and storage to a drawable related code.
    // It does not seem right to store something localized in a global state
    // fix here and in recordLayers
    const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
    Vector3 transformedLightCenter(savedLightCenter);
    // map current light center into RenderNode's coordinate space
    layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
    LightingInfo::setLightCenterRaw(transformedLightCenter);

    const RenderProperties& properties = layerNode->properties();
    const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
    if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) {
        return false;
    }

    ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(),
                  bounds.height());

    layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false;
    layerCanvas->clear(SK_ColorTRANSPARENT);

    RenderNodeDrawable root(layerNode, layerCanvas, false);
    root.forceDraw(layerCanvas);
    layerCanvas->restoreToCount(saveCount);

    LightingInfo::setLightCenterRaw(savedLightCenter);
    return true;
}

static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) {
    CommonPool::post([data, filename] {
        if (0 == access(filename.c_str(), F_OK)) {
            return;
        }

        SkFILEWStream stream(filename.c_str());
        if (stream.isValid()) {
            stream.write(data->data(), data->size());
            stream.flush();
            ALOGD("SKP Captured Drawing Output (%zu bytes) for frame. %s", stream.bytesWritten(),
                     filename.c_str());
        }
    });
}

// Note multiple SkiaPipeline instances may be loaded if more than one app is visible.
// Each instance may observe the filename changing and try to record to a file of the same name.
// Only the first one will succeed. There is no scope available here where we could coordinate
// to cause this function to return true for only one of the instances.
bool SkiaPipeline::shouldStartNewFileCapture() {
    // Don't start a new file based capture if one is currently ongoing.
    if (mCaptureMode != CaptureMode::None) { return false; }

    // A new capture is started when the filename property changes.
    // Read the filename property.
    std::string prop = base::GetProperty(PROPERTY_CAPTURE_SKP_FILENAME, "0");
    // if the filename property changed to a valid value
    if (prop[0] != '0' && mCapturedFile != prop) {
        // remember this new filename
        mCapturedFile = prop;
        // and get a property indicating how many frames to capture.
        mCaptureSequence = base::GetIntProperty(PROPERTY_CAPTURE_SKP_FRAMES, 1);
        if (mCaptureSequence <= 0) {
            return false;
        } else if (mCaptureSequence == 1) {
            mCaptureMode = CaptureMode::SingleFrameSKP;
        } else {
            mCaptureMode = CaptureMode::MultiFrameSKP;
        }
        return true;
    }
    return false;
}

// performs the first-frame work of a multi frame SKP capture. Returns true if successful.
bool SkiaPipeline::setupMultiFrameCapture() {
    ALOGD("Set up multi-frame capture, frames = %d", mCaptureSequence);
    // We own this stream and need to hold it until close() finishes.
    auto stream = std::make_unique<SkFILEWStream>(mCapturedFile.c_str());
    if (stream->isValid()) {
        mOpenMultiPicStream = std::move(stream);
        mSerialContext.reset(new SkSharingSerialContext());
        SkSerialProcs procs;
        procs.fImageProc = SkSharingSerialContext::serializeImage;
        procs.fImageCtx = mSerialContext.get();
        procs.fTypefaceProc = [](SkTypeface* tf, void* ctx){
            return tf->serialize(SkTypeface::SerializeBehavior::kDoIncludeData);
        };
        // SkDocuments don't take owership of the streams they write.
        // we need to keep it until after mMultiPic.close()
        // procs is passed as a pointer, but just as a method of having an optional default.
        // procs doesn't need to outlive this Make call.
        mMultiPic = SkMultiPictureDocument::Make(mOpenMultiPicStream.get(), &procs,
            [sharingCtx = mSerialContext.get()](const SkPicture* pic) {
                    SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx);
            });
        return true;
    } else {
        ALOGE("Could not open \"%s\" for writing.", mCapturedFile.c_str());
        mCaptureSequence = 0;
        mCaptureMode = CaptureMode::None;
        return false;
    }
}

// recurse through the rendernode's children, add any nodes which are layers to the queue.
static void collectLayers(RenderNode* node, LayerUpdateQueue* layers) {
    SkiaDisplayList* dl = node->getDisplayList().asSkiaDl();
    if (dl) {
        const auto& prop = node->properties();
        if (node->hasLayer()) {
            layers->enqueueLayerWithDamage(node, Rect(prop.getWidth(), prop.getHeight()));
        }
        // The way to recurse through rendernodes is to call this with a lambda.
        dl->updateChildren([&](RenderNode* child) { collectLayers(child, layers); });
    }
}

// record the provided layers to the provided canvas as self-contained skpictures.
static void recordLayers(const LayerUpdateQueue& layers,
    SkCanvas* mskpCanvas) {
    const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
    // Record the commands to re-draw each dirty layer into an SkPicture
    for (size_t i = 0; i < layers.entries().size(); i++) {
        RenderNode* layerNode = layers.entries()[i].renderNode.get();
        const Rect& layerDamage = layers.entries()[i].damage;
        const RenderProperties& properties = layerNode->properties();

        // Temporarily map current light center into RenderNode's coordinate space
        Vector3 transformedLightCenter(savedLightCenter);
        layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
        LightingInfo::setLightCenterRaw(transformedLightCenter);

        SkPictureRecorder layerRec;
        auto* recCanvas = layerRec.beginRecording(properties.getWidth(),
            properties.getHeight());
        // This is not recorded but still causes clipping.
        recCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());
        RenderNodeDrawable root(layerNode, recCanvas, false);
        root.forceDraw(recCanvas);
        // Now write this picture into the SKP canvas with an annotation indicating what it is
        mskpCanvas->drawAnnotation(layerDamage.toSkRect(), String8::format(
            "OffscreenLayerDraw|%" PRId64, layerNode->uniqueId()).c_str(), nullptr);
        mskpCanvas->drawPicture(layerRec.finishRecordingAsPicture());
    }
    LightingInfo::setLightCenterRaw(savedLightCenter);
}

SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface, RenderNode* root,
    const LayerUpdateQueue& dirtyLayers) {
    if (CC_LIKELY(!Properties::skpCaptureEnabled)) {
        return surface->getCanvas(); // Bail out early when capture is not turned on.
    }
    // Note that shouldStartNewFileCapture tells us if this is the *first* frame of a capture.
    bool firstFrameOfAnim = false;
    if (shouldStartNewFileCapture() && mCaptureMode == CaptureMode::MultiFrameSKP) {
        // set a reminder to record every layer near the end of this method, after we have set up
        // the nway canvas.
        firstFrameOfAnim = true;
        if (!setupMultiFrameCapture()) {
            return surface->getCanvas();
        }
    }

    // Create a canvas pointer, fill it depending on what kind of capture is requested (if any)
    SkCanvas* pictureCanvas = nullptr;
    switch (mCaptureMode) {
        case CaptureMode::CallbackAPI:
        case CaptureMode::SingleFrameSKP:
            mRecorder.reset(new SkPictureRecorder());
            pictureCanvas = mRecorder->beginRecording(surface->width(), surface->height());
            break;
        case CaptureMode::MultiFrameSKP:
            // If a multi frame recording is active, initialize recording for a single frame of a
            // multi frame file.
            pictureCanvas = mMultiPic->beginPage(surface->width(), surface->height());
            break;
        case CaptureMode::None:
            // Returning here in the non-capture case means we can count on pictureCanvas being
            // non-null below.
            return surface->getCanvas();
    }

    // Setting up an nway canvas is common to any kind of capture.
    mNwayCanvas = std::make_unique<SkNWayCanvas>(surface->width(), surface->height());
    mNwayCanvas->addCanvas(surface->getCanvas());
    mNwayCanvas->addCanvas(pictureCanvas);

    if (firstFrameOfAnim) {
        // On the first frame of any mskp capture we want to record any layers that are needed in
        // frame but may have been rendered offscreen before recording began.
        // We do not maintain a list of all layers, since it isn't needed outside this rare,
        // recording use case. Traverse the tree to find them and put them in this LayerUpdateQueue.
        LayerUpdateQueue luq;
        collectLayers(root, &luq);
        recordLayers(luq, mNwayCanvas.get());
    } else {
        // on non-first frames, we record any normal layer draws (dirty regions)
        recordLayers(dirtyLayers, mNwayCanvas.get());
    }

    return mNwayCanvas.get();
}

void SkiaPipeline::endCapture(SkSurface* surface) {
    if (CC_LIKELY(mCaptureMode == CaptureMode::None)) { return; }
    mNwayCanvas.reset();
    ATRACE_CALL();
    if (mCaptureSequence > 0 && mCaptureMode == CaptureMode::MultiFrameSKP) {
        mMultiPic->endPage();
        mCaptureSequence--;
        if (mCaptureSequence == 0) {
            mCaptureMode = CaptureMode::None;
            // Pass mMultiPic and mOpenMultiPicStream to a background thread, which will handle
            // the heavyweight serialization work and destroy them. mOpenMultiPicStream is released
            // to a bare pointer because keeping it in a smart pointer makes the lambda
            // non-copyable. The lambda is only called once, so this is safe.
            SkFILEWStream* stream = mOpenMultiPicStream.release();
            CommonPool::post([doc = std::move(mMultiPic), stream]{
                ALOGD("Finalizing multi frame SKP");
                doc->close();
                delete stream;
                ALOGD("Multi frame SKP complete.");
            });
        }
    } else {
        sk_sp<SkPicture> picture = mRecorder->finishRecordingAsPicture();
        if (picture->approximateOpCount() > 0) {
            if (mPictureCapturedCallback) {
                std::invoke(mPictureCapturedCallback, std::move(picture));
            } else {
                // single frame skp to file
                SkSerialProcs procs;
                procs.fTypefaceProc = [](SkTypeface* tf, void* ctx){
                    return tf->serialize(SkTypeface::SerializeBehavior::kDoIncludeData);
                };
                procs.fImageProc = [](SkImage* img, void* ctx) -> sk_sp<SkData> {
                    GrDirectContext* dCtx = static_cast<GrDirectContext*>(ctx);
                    return SkPngEncoder::Encode(dCtx,
                                                img,
                                                SkPngEncoder::Options{});
                };
                procs.fImageCtx = mRenderThread.getGrContext();
                auto data = picture->serialize(&procs);
                savePictureAsync(data, mCapturedFile);
                mCaptureSequence = 0;
                mCaptureMode = CaptureMode::None;
            }
        }
        mRecorder.reset();
    }
}

void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip,
                               const std::vector<sp<RenderNode>>& nodes, bool opaque,
                               const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                               const SkMatrix& preTransform) {
    bool previousSkpEnabled = Properties::skpCaptureEnabled;
    if (mPictureCapturedCallback) {
        Properties::skpCaptureEnabled = true;
    }

    // Initialize the canvas for the current frame, that might be a recording canvas if SKP
    // capture is enabled.
    SkCanvas* canvas = tryCapture(surface.get(), nodes[0].get(), layers);

    // draw all layers up front
    renderLayersImpl(layers, opaque);

    renderFrameImpl(clip, nodes, opaque, contentDrawBounds, canvas, preTransform);

    endCapture(surface.get());

    if (CC_UNLIKELY(Properties::debugOverdraw)) {
        renderOverdraw(clip, nodes, contentDrawBounds, surface, preTransform);
    }

    Properties::skpCaptureEnabled = previousSkpEnabled;
}

namespace {
static Rect nodeBounds(RenderNode& node) {
    auto& props = node.properties();
    return Rect(props.getLeft(), props.getTop(), props.getRight(), props.getBottom());
}
}  // namespace

void SkiaPipeline::renderFrameImpl(const SkRect& clip,
                                   const std::vector<sp<RenderNode>>& nodes, bool opaque,
                                   const Rect& contentDrawBounds, SkCanvas* canvas,
                                   const SkMatrix& preTransform) {
    SkAutoCanvasRestore saver(canvas, true);
    auto clipRestriction = preTransform.mapRect(clip).roundOut();
    if (CC_UNLIKELY(isCapturingSkp())) {
        canvas->drawAnnotation(SkRect::Make(clipRestriction), "AndroidDeviceClipRestriction",
            nullptr);
    } else {
        // clip drawing to dirty region only when not recording SKP files (which should contain all
        // draw ops on every frame)
        canvas->androidFramework_setDeviceClipRestriction(clipRestriction);
    }
    canvas->concat(preTransform);

    if (!opaque) {
        canvas->clear(SK_ColorTRANSPARENT);
    }

    if (1 == nodes.size()) {
        if (!nodes[0]->nothingToDraw()) {
            RenderNodeDrawable root(nodes[0].get(), canvas);
            root.draw(canvas);
        }
    } else if (0 == nodes.size()) {
        // nothing to draw
    } else {
        // It there are multiple render nodes, they are laid out as follows:
        // #0 - backdrop (content + caption)
        // #1 - content (local bounds are at (0,0), will be translated and clipped to backdrop)
        // #2 - additional overlay nodes
        // Usually the backdrop cannot be seen since it will be entirely covered by the content.
        // While
        // resizing however it might become partially visible. The following render loop will crop
        // the
        // backdrop against the content and draw the remaining part of it. It will then draw the
        // content
        // cropped to the backdrop (since that indicates a shrinking of the window).
        //
        // Additional nodes will be drawn on top with no particular clipping semantics.

        // Usually the contents bounds should be mContentDrawBounds - however - we will
        // move it towards the fixed edge to give it a more stable appearance (for the moment).
        // If there is no content bounds we ignore the layering as stated above and start with 2.

        // Backdrop bounds in render target space
        const Rect backdrop = nodeBounds(*nodes[0]);

        // Bounds that content will fill in render target space (note content node bounds may be
        // bigger)
        Rect content(contentDrawBounds.getWidth(), contentDrawBounds.getHeight());
        content.translate(backdrop.left, backdrop.top);
        if (!content.contains(backdrop) && !nodes[0]->nothingToDraw()) {
            // Content doesn't entirely overlap backdrop, so fill around content (right/bottom)

            // Note: in the future, if content doesn't snap to backdrop's left/top, this may need to
            // also fill left/top. Currently, both 2up and freeform position content at the top/left
            // of
            // the backdrop, so this isn't necessary.
            RenderNodeDrawable backdropNode(nodes[0].get(), canvas);
            if (content.right < backdrop.right) {
                // draw backdrop to right side of content
                SkAutoCanvasRestore acr(canvas, true);
                canvas->clipRect(SkRect::MakeLTRB(content.right, backdrop.top, backdrop.right,
                                                  backdrop.bottom));
                backdropNode.draw(canvas);
            }
            if (content.bottom < backdrop.bottom) {
                // draw backdrop to bottom of content
                // Note: bottom fill uses content left/right, to avoid overdrawing left/right fill
                SkAutoCanvasRestore acr(canvas, true);
                canvas->clipRect(SkRect::MakeLTRB(content.left, content.bottom, content.right,
                                                  backdrop.bottom));
                backdropNode.draw(canvas);
            }
        }

        RenderNodeDrawable contentNode(nodes[1].get(), canvas);
        if (!backdrop.isEmpty()) {
            // content node translation to catch up with backdrop
            float dx = backdrop.left - contentDrawBounds.left;
            float dy = backdrop.top - contentDrawBounds.top;

            SkAutoCanvasRestore acr(canvas, true);
            canvas->translate(dx, dy);
            const SkRect contentLocalClip =
                    SkRect::MakeXYWH(contentDrawBounds.left, contentDrawBounds.top,
                                     backdrop.getWidth(), backdrop.getHeight());
            canvas->clipRect(contentLocalClip);
            contentNode.draw(canvas);
        } else {
            SkAutoCanvasRestore acr(canvas, true);
            contentNode.draw(canvas);
        }

        // remaining overlay nodes, simply defer
        for (size_t index = 2; index < nodes.size(); index++) {
            if (!nodes[index]->nothingToDraw()) {
                SkAutoCanvasRestore acr(canvas, true);
                RenderNodeDrawable overlayNode(nodes[index].get(), canvas);
                overlayNode.draw(canvas);
            }
        }
    }
}

void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
    mColorMode = colorMode;
    switch (colorMode) {
        case ColorMode::Default:
            mSurfaceColorType = SkColorType::kN32_SkColorType;
            mSurfaceColorSpace = SkColorSpace::MakeSRGB();
            break;
        case ColorMode::WideColorGamut:
            mSurfaceColorType = DeviceInfo::get()->getWideColorType();
            mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
            break;
        case ColorMode::Hdr:
            if (DeviceInfo::get()->isSupportRgba10101010ForHdr()) {
                mSurfaceColorType = SkColorType::kRGBA_10x6_SkColorType;
                mSurfaceColorSpace = SkColorSpace::MakeRGB(
                        GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
            } else if (DeviceInfo::get()->isSupportFp16ForHdr()) {
                mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
                mSurfaceColorSpace = SkColorSpace::MakeSRGB();
            } else {
                mSurfaceColorType = SkColorType::kN32_SkColorType;
                mSurfaceColorSpace = SkColorSpace::MakeRGB(
                        GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
            }
            break;
        case ColorMode::Hdr10:
            mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
            mSurfaceColorSpace = SkColorSpace::MakeRGB(
                    GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
            break;
        case ColorMode::A8:
            mSurfaceColorType = SkColorType::kAlpha_8_SkColorType;
            mSurfaceColorSpace = nullptr;
            break;
    }
}

void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
    if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
        mTargetSdrHdrRatio = ratio;

        if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr() &&
            !DeviceInfo::get()->isSupportRgba10101010ForHdr()) {
            mSurfaceColorSpace = SkColorSpace::MakeSRGB();
        } else {
            mSurfaceColorSpace = SkColorSpace::MakeRGB(
                    GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
        }
    } else {
        mTargetSdrHdrRatio = 1.f;
    }
}

// Overdraw debugging

// These colors should be kept in sync with Caches::getOverdrawColor() with a few differences.
// This implementation requires transparent entries for "no overdraw" and "single draws".
static const SkColor kOverdrawColors[2][6] = {
    {
        0x00000000,
        0x00000000,
        0x2f0000ff,
        0x2f00ff00,
        0x3fff0000,
        0x7fff0000,
    },
    {
        0x00000000,
        0x00000000,
        0x2f0000ff,
        0x4fffff00,
        0x5fff89d7,
        0x7fff0000,
    },
};

void SkiaPipeline::renderOverdraw(const SkRect& clip,
                                  const std::vector<sp<RenderNode>>& nodes,
                                  const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                                  const SkMatrix& preTransform) {
    // Set up the overdraw canvas.
    SkImageInfo offscreenInfo = SkImageInfo::MakeA8(surface->width(), surface->height());
    sk_sp<SkSurface> offscreen = surface->makeSurface(offscreenInfo);
    LOG_ALWAYS_FATAL_IF(!offscreen, "Failed to create offscreen SkSurface for overdraw viz.");
    SkOverdrawCanvas overdrawCanvas(offscreen->getCanvas());

    // Fake a redraw to replay the draw commands.  This will increment the alpha channel
    // each time a pixel would have been drawn.
    // Pass true for opaque so we skip the clear - the overdrawCanvas is already zero
    // initialized.
    renderFrameImpl(clip, nodes, true, contentDrawBounds, &overdrawCanvas, preTransform);
    sk_sp<SkImage> counts = offscreen->makeImageSnapshot();

    // Draw overdraw colors to the canvas.  The color filter will convert counts to colors.
    SkPaint paint;
    const SkColor* colors = kOverdrawColors[static_cast<int>(Properties::overdrawColorSet)];
    paint.setColorFilter(SkOverdrawColorFilter::MakeWithSkColors(colors));
    surface->getCanvas()->drawImage(counts.get(), 0.0f, 0.0f, SkSamplingOptions(), &paint);
}

} /* namespace skiapipeline */
} /* namespace uirenderer */
} /* namespace android */
