/*
 * Copyright 2021 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
#include "src/gpu/graphite/Device.h"

#include "include/core/SkAlphaType.h"
#include "include/core/SkArc.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkClipOp.h"
#include "include/core/SkColorFilter.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkM44.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPath.h"
#include "include/core/SkPathBuilder.h"
#include "include/core/SkPathEffect.h"
#include "include/core/SkPixmap.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRRect.h"
#include "include/core/SkRegion.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkSize.h"
#include "include/core/SkStrokeRec.h"
#include "include/core/SkSurfaceProps.h"
#include "include/core/SkVertices.h"
#include "include/gpu/GpuTypes.h"
#include "include/gpu/graphite/Context.h"
#include "include/gpu/graphite/GraphiteTypes.h"
#include "include/gpu/graphite/Image.h"
#include "include/gpu/graphite/Recorder.h"
#include "include/gpu/graphite/Surface.h"
#include "include/gpu/graphite/TextureInfo.h"
#include "include/private/base/SingleOwner.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkFloatingPoint.h"
#include "include/private/base/SkTo.h"
#include "src/base/SkArenaAlloc.h"
#include "src/base/SkVx.h"
#include "src/core/SkBlenderBase.h"
#include "src/core/SkImageFilterTypes.h"  // IWYU pragma: keep
#include "src/core/SkImagePriv.h"
#include "src/core/SkPaintPriv.h"
#include "src/core/SkPathPriv.h"
#include "src/core/SkRRectPriv.h"
#include "src/core/SkRectPriv.h"
#include "src/core/SkSpecialImage.h"
#include "src/core/SkStrikeCache.h"
#include "src/core/SkTraceEvent.h"
#include "src/core/SkVerticesPriv.h"
#include "src/gpu/AtlasTypes.h"
#include "src/gpu/BlurUtils.h"
#include "src/gpu/SkBackingFit.h"
#include "src/gpu/Swizzle.h"
#include "src/gpu/TiledTextureUtils.h"
#include "src/gpu/graphite/AtlasProvider.h"
#include "src/gpu/graphite/Caps.h"
#include "src/gpu/graphite/ContextOptionsPriv.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/ContextUtils.h"
#include "src/gpu/graphite/DrawContext.h"
#include "src/gpu/graphite/DrawList.h"
#include "src/gpu/graphite/DrawParams.h"
#include "src/gpu/graphite/DrawTypes.h"
#include "src/gpu/graphite/Image_Base_Graphite.h"
#include "src/gpu/graphite/Image_Graphite.h"
#include "src/gpu/graphite/Log.h"
#include "src/gpu/graphite/PaintParams.h"
#include "src/gpu/graphite/PathAtlas.h"
#include "src/gpu/graphite/RasterPathAtlas.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/Renderer.h"
#include "src/gpu/graphite/RendererProvider.h"
#include "src/gpu/graphite/SpecialImage_Graphite.h"
#include "src/gpu/graphite/TextureFormat.h"
#include "src/gpu/graphite/TextureInfoPriv.h"
#include "src/gpu/graphite/TextureProxy.h"
#include "src/gpu/graphite/TextureProxyView.h"
#include "src/gpu/graphite/TextureUtils.h"
#include "src/gpu/graphite/geom/AnalyticBlurMask.h"
#include "src/gpu/graphite/geom/BoundsManager.h"
#include "src/gpu/graphite/geom/CoverageMaskShape.h"
#include "src/gpu/graphite/geom/EdgeAAQuad.h"
#include "src/gpu/graphite/geom/Geometry.h"
#include "src/gpu/graphite/geom/IntersectionTree.h"
#include "src/gpu/graphite/geom/NonMSAAClip.h"
#include "src/gpu/graphite/geom/Shape.h"
#include "src/gpu/graphite/geom/SubRunData.h"
#include "src/gpu/graphite/geom/Transform.h"
#include "src/gpu/graphite/task/Task.h"
#include "src/gpu/graphite/task/UploadTask.h"
#include "src/image/SkImage_Base.h"
#include "src/text/gpu/GlyphVector.h"
#include "src/text/gpu/SlugImpl.h"
#include "src/text/gpu/SubRunContainer.h"
#include "src/text/gpu/TextBlobRedrawCoordinator.h"
#include "src/text/gpu/VertexFiller.h"

#include <algorithm>
#include <atomic>
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>

using RescaleGamma       = SkImage::RescaleGamma;
using RescaleMode        = SkImage::RescaleMode;
using ReadPixelsCallback = SkImage::ReadPixelsCallback;
using ReadPixelsContext  = SkImage::ReadPixelsContext;

#if defined(GPU_TEST_UTILS)
int gOverrideMaxTextureSizeGraphite = 0;
// Allows tests to check how many tiles were drawn on the most recent call to
// Device::drawAsTiledImageRect. This is an atomic because we can write to it from
// multiple threads during "normal" operations. However, the tests that actually
// read from it are done single-threaded.
std::atomic<int> gNumTilesDrawnGraphite{0};
#endif

namespace skgpu::graphite {

class Recording;
enum class TextureFormat : uint8_t;

#define ASSERT_SINGLE_OWNER SkASSERT(fRecorder); SKGPU_ASSERT_SINGLE_OWNER(fRecorder->singleOwner())

namespace {

const SkStrokeRec& DefaultFillStyle() {
    static const SkStrokeRec kFillStyle(SkStrokeRec::kFill_InitStyle);
    return kFillStyle;
}

bool paint_depends_on_dst(const PaintParams& paintParams) {
    std::optional<SkBlendMode> bm = paintParams.asFinalBlendMode();
    if (!bm.has_value()) {
        return true; // Runtime blenders always depend on the dst
    }

    if (bm == SkBlendMode::kClear || bm == SkBlendMode::kSrc) {
        // src and clear blending never depend on dst
        return false;
    } else if (bm != SkBlendMode::kSrcOver && bm != SkBlendMode::kDstOut) {
        // any other blend mode besides src-over and dst-out use dst in some way
        return true;
    }

    // At this point, we depend on the dst if source alpha != 1, so analyze the paint to
    // see if it's opaque.
    bool srcIsTransparent = !paintParams.color().isOpaque() ||
                                  (paintParams.shader() && !paintParams.shader()->isOpaque()) ||
                                  (paintParams.colorFilter() &&
                                        !paintParams.colorFilter()->isAlphaUnchanged());

    if (paintParams.primitiveBlender()) {
        std::optional<SkBlendMode> primBlend = as_BB(paintParams.primitiveBlender())->asBlendMode();
        // The primitive blender does not blend against the dst color, but it might change whether
        // or not the src is transparent.
        if (primBlend && !srcIsTransparent) {
            // Since dst might be transparent, we can only preserve opacity for cases where the
            // src coefficient is one and the dst coefficient is zero (when src alpha = 1).
            srcIsTransparent = primBlend != SkBlendMode::kSrcOver && primBlend != SkBlendMode::kSrc;
        } else {
            // Runtime blender or complex blend modifies the final src color so assume it has alpha
            srcIsTransparent = true;
        }
    }
    return srcIsTransparent;
}

/** If the paint can be reduced to a solid flood-fill, determine the correct color to fill with. */
std::optional<SkColor4f> extract_paint_color(const PaintParams& paint,
                                             const SkColorInfo& dstColorInfo) {
    SkASSERT(!paint_depends_on_dst(paint));

    std::optional<SkBlendMode> bm = paint.asFinalBlendMode();
    // Since we don't depend on the dst, a dst-out blend mode implies source is
    // opaque, which causes dst-out to behave like clear.
    if (bm == SkBlendMode::kClear || bm == SkBlendMode::kDstOut) {
        return SkColors::kTransparent;
    }

    // PaintParams has already consolidated constant shaders and applied color filters to constant
    // input colors. If the paint still has any of those fields, then we can't extract it.
    if (paint.shader() || paint.colorFilter()) {
        return std::nullopt;
    }

    // However, PaintParams converted the color in sRGB and we need to return this in the
    // destination color space.
    return PaintParams::Color4fPrepForDst(paint.color(), dstColorInfo);
}

// Returns a local rect that has been adjusted such that when it's rasterized with `localToDevice`
// it will be pixel aligned. If this adjustment is not possible (due to transform type or precision)
// then this returns the original local rect unmodified.
//
// If `strokeWidth` is null, it's assumed to be a filled rectangle. If it's not null, on input it
// should hold the stroke width (or 0 for a hairline). After this returns, the stroke width may
// have been adjusted so that outer and inner stroked edges are pixel aligned (in which case the
// underlying rectangle geometry probably won't be pixel aligned).
//
// A best effort is made to align the stroke edges when there's a non-uniform scale factor that
// prevents exactly aligning both X and Y axes.
Rect snap_rect_to_pixels(const Transform& localToDevice,
                         const Rect& rect,
                         float* strokeWidth=nullptr) {
    if (localToDevice.type() > Transform::Type::kRectStaysRect) {
        return rect;
    }

    Rect snappedDeviceRect;
    if (!strokeWidth) {
        // Just a fill, use round() to emulate non-AA rasterization (vs. roundOut() to get the
        // covering bounds). This matches how ClipStack treats clipRects with PixelSnapping::kYes.
        snappedDeviceRect = localToDevice.mapRect(rect).round();
    } else if (strokeWidth) {
        if (*strokeWidth == 0.f) {
            // Hairline case needs to be outset by 1/2 device pixels *before* rounding, and then
            // inset by 1/2px to get the base shape while leaving the stroke width as 0.
            snappedDeviceRect = localToDevice.mapRect(rect);
            snappedDeviceRect.outset(0.5f).round().inset(0.5f);
        } else {
            // For regular strokes, outset by the stroke radius *before* mapping to device space,
            // and then round.
            snappedDeviceRect = localToDevice.mapRect(rect.makeOutset(0.5f*(*strokeWidth))).round();

            // devScales.x() holds scale factor affecting device-space X axis (so max of |m00| or
            // |m01|) and y() holds the device Y axis scale (max of |m10| or |m11|).
            skvx::float2 devScales = max(abs(skvx::float2(localToDevice.matrix().rc(0,0),
                                                          localToDevice.matrix().rc(1,0))),
                                         abs(skvx::float2(localToDevice.matrix().rc(0,1),
                                                          localToDevice.matrix().rc(1,1))));
            skvx::float2 devStrokeWidth = max(round(*strokeWidth * devScales), 1.f);

            // Prioritize the axis that has the largest device-space radius (any error from a
            // non-uniform scale factor will go into the inner edge of the opposite axis).
            // During animating scale factors, preserving the large axis leads to better behavior.
            if (devStrokeWidth.x() > devStrokeWidth.y()) {
                *strokeWidth = devStrokeWidth.x() / devScales.x();
            } else {
                *strokeWidth = devStrokeWidth.y() / devScales.y();
            }

            snappedDeviceRect.inset(0.5f * devScales * (*strokeWidth));
        }
    }

    // Map back to local space so that it can be drawn with appropriate coord interpolation.
    Rect snappedLocalRect = localToDevice.inverseMapRect(snappedDeviceRect);
    // If the transform has an extreme scale factor or large translation, it's possible for floating
    // point precision to round `snappedLocalRect` in such a way that re-transforming it by the
    // local-to-device matrix no longer matches the expected device bounds.
    if (snappedDeviceRect.nearlyEquals(localToDevice.mapRect(snappedLocalRect))) {
        return snappedLocalRect;
    } else {
        // In this case we will just return the original geometry and the pixels will show
        // fractional coverage.
        return rect;
    }
}

// If possible, snaps `dstRect` such that its device-space transformation lands on pixel bounds,
// and then updates `srcRect` to match the original src-to-dst coordinate mapping.
void snap_src_and_dst_rect_to_pixels(const Transform& localToDevice,
                                     SkRect* srcRect,
                                     SkRect* dstRect) {
    if (localToDevice.type() > Transform::Type::kRectStaysRect) {
        return;
    }

    // Assume snapping will succeed and always update 'src' to match; in the event snapping
    // returns the original dst rect, then the recalculated src rect is a no-op.
    SkMatrix dstToSrc = SkMatrix::RectToRectOrIdentity(*dstRect, *srcRect);
    *dstRect = snap_rect_to_pixels(localToDevice, *dstRect).asSkRect();
    *srcRect = dstToSrc.mapRect(*dstRect);
}

// Returns the inner bounds of `geometry` that is known to have full coverage. This does not worry
// about identifying draws that are equivalent pixel aligned and thus entirely full coverage, as
// that should have been caught earlier and used a coverage-less renderer from the beginning.
//
// An empty Rect is returned if there is no available inner bounds, or if it's not worth performing.
Rect get_inner_bounds(const Geometry& geometry, const Transform& localToDevice) {
    auto applyAAInset = [&](Rect rect) {
        // If the aa inset is too large, rect becomes empty and the inner bounds draw is
        // automatically skipped
        float aaInset = localToDevice.localAARadius(rect);
        rect.inset(aaInset);
        // Only add a second draw if it will have a reasonable number of covered pixels; otherwise
        // we are just adding draws to sort and pipelines to switch around.
        static constexpr float kInnerFillArea = 64*64;
        // Approximate the device-space area based on the minimum scale factor of the transform.
        float scaleFactor = sk_ieee_float_divide(1.f, aaInset);
        return scaleFactor*rect.area() >= kInnerFillArea ? rect : Rect::InfiniteInverted();
    };

    if (geometry.isEdgeAAQuad()) {
        const EdgeAAQuad& quad = geometry.edgeAAQuad();
        if (quad.isRect()) {
            return applyAAInset(quad.bounds());
        }
        // else currently we don't have a function to calculate the largest interior axis aligned
        // bounding box of a quadrilateral so skip the inner fill draw.
    } else if (geometry.isShape()) {
        const Shape& shape = geometry.shape();
        if (shape.isRect()) {
            return applyAAInset(shape.rect());
        } else if (shape.isRRect()) {
            return applyAAInset(SkRRectPriv::InnerBounds(shape.rrect()));
        }
    }

    return Rect::InfiniteInverted();
}

SkIRect rect_to_pixelbounds(const Rect& r) {
    return r.makeRoundOut().asSkIRect();
}

bool is_pixel_aligned(const Rect& r, const Transform& t) {
    if (t.type() <= Transform::Type::kRectStaysRect) {
        Rect devRect = t.mapRect(r);
        return devRect.nearlyEquals(devRect.makeRound());
    }

    return false;
}

bool is_simple_shape(const Shape& shape, SkStrokeRec::Style type) {
    // We send regular filled and hairline [round] rectangles, stroked/hairline lines, and stroked
    // [r]rects with circular corners to a single Renderer that does not trigger MSAA.
    // Per-edge AA quadrilaterals also use the same Renderer but those are not "Shapes".
    // These shapes and quads may also be combined with a second non-AA inner fill. This fill step
    // is also directly used for flooding the clip
    return shape.isFloodFill() ||
           (!shape.inverted() && type != SkStrokeRec::kStrokeAndFill_Style &&
            (shape.isRect() ||
             (shape.isLine() && type != SkStrokeRec::kFill_Style) ||
             (shape.isRRect() && (type != SkStrokeRec::kStroke_Style ||
                                  SkRRectPriv::AllCornersCircular(shape.rrect())))));
}

bool use_compute_atlas_when_available(PathRendererStrategy strategy) {
    return strategy == PathRendererStrategy::kComputeAnalyticAA ||
           strategy == PathRendererStrategy::kComputeMSAA16 ||
           strategy == PathRendererStrategy::kComputeMSAA8 ||
           strategy == PathRendererStrategy::kDefault;
}

} // anonymous namespace

/**
 * IntersectionTreeSet controls multiple IntersectionTrees to organize all add rectangles into
 * disjoint sets. For a given CompressedPaintersOrder and bounds, it returns the smallest
 * DisjointStencilIndex that guarantees the bounds are disjoint from all other draws that use the
 * same painters order and stencil index.
 */
class Device::IntersectionTreeSet {
public:
    IntersectionTreeSet() = default;

    DisjointStencilIndex add(CompressedPaintersOrder drawOrder, Rect rect) {
        auto& trees = fTrees[drawOrder];
        DisjointStencilIndex stencil = DrawOrder::kUnassigned.next();
        for (auto&& tree : trees) {
            if (tree->add(rect)) {
                return stencil;
            }
            stencil = stencil.next(); // advance to the next tree's index
        }

        // If here, no existing intersection tree can hold the rect so add a new one
        IntersectionTree* newTree = this->makeTree();
        SkAssertResult(newTree->add(rect));
        trees.push_back(newTree);
        return stencil;
    }

    void reset() {
        fTrees.clear();
        fTreeStore.reset();
    }

private:
    struct Hash {
        size_t operator()(const CompressedPaintersOrder& o) const noexcept { return o.bits(); }
    };

    IntersectionTree* makeTree() {
        return fTreeStore.make<IntersectionTree>();
    }

    // Each compressed painters order defines a barrier around draws so each order's set of draws
    // are independent, even if they may intersect. Within each order, the list of trees holds the
    // IntersectionTrees representing each disjoint set.
    // TODO: This organization of trees is logically convenient but may need to be optimized based
    // on real world data (e.g. how sparse is the map, how long is each vector of trees,...)
    std::unordered_map<CompressedPaintersOrder, std::vector<IntersectionTree*>, Hash> fTrees;
    SkSTArenaAllocWithReset<4 * sizeof(IntersectionTree)> fTreeStore;
};

sk_sp<Device> Device::Make(Recorder* recorder,
                           const SkImageInfo& ii,
                           skgpu::Budgeted budgeted,
                           Mipmapped mipmapped,
                           SkBackingFit backingFit,
                           const SkSurfaceProps& props,
                           LoadOp initialLoadOp,
                           std::string_view label,
                           bool registerWithRecorder) {
    SkASSERT(!(mipmapped == Mipmapped::kYes && backingFit == SkBackingFit::kApprox));
    if (!recorder) {
        return nullptr;
    }

    const Caps* caps = recorder->priv().caps();
    SkISize backingDimensions = backingFit == SkBackingFit::kApprox ? GetApproxSize(ii.dimensions())
                                                                    : ii.dimensions();
    auto textureInfo = caps->getDefaultSampledTextureInfo(ii.colorType(),
                                                          mipmapped,
                                                          recorder->priv().isProtected(),
                                                          Renderable::kYes);

    return Make(recorder,
                TextureProxy::Make(caps, recorder->priv().resourceProvider(),
                                   backingDimensions, textureInfo, std::move(label), budgeted),
                ii.dimensions(),
                ii.colorInfo(),
                props,
                initialLoadOp,
                registerWithRecorder);
}

sk_sp<Device> Device::Make(Recorder* recorder,
                           sk_sp<TextureProxy> target,
                           SkISize deviceSize,
                           const SkColorInfo& colorInfo,
                           const SkSurfaceProps& props,
                           LoadOp initialLoadOp,
                           bool registerWithRecorder) {
    if (!recorder) {
        return nullptr;
    }

    sk_sp<DrawContext> dc = DrawContext::Make(recorder->priv().caps(),
                                              std::move(target),
                                              deviceSize,
                                              colorInfo,
                                              props);
    if (!dc) {
        return nullptr;
    } else if (initialLoadOp == LoadOp::kClear) {
        dc->clear(SkColors::kTransparent);
    } else if (initialLoadOp == LoadOp::kDiscard) {
        dc->discard();
    } // else kLoad is the default initial op for a DrawContext

    sk_sp<Device> device{new Device(recorder, std::move(dc))};
    if (registerWithRecorder) {
        // We don't register the device with the recorder until after the constructor has returned.
        recorder->registerDevice(device);
    } else {
        // Since it's not registered, it should go out of scope before nextRecordingID() changes
        // from what is saved to fScopedRecordingID.
        SkDEBUGCODE(device->fScopedRecordingID = recorder->priv().nextRecordingID();)
    }
    return device;
}

// These default tuning numbers for the HybridBoundsManager were chosen from looking at performance
// and accuracy curves produced by the BoundsManagerBench for random draw bounding boxes. This
// config will use brute force for the first 64 draw calls to the Device and then switch to a grid
// that is dynamically sized to produce cells that are 16x16, up to a grid that's 32x32 cells.
// This seemed like a sweet spot balancing accuracy for low-draw count surfaces and overhead for
// high-draw count and high-resolution surfaces. With the 32x32 grid limit, cell size will increase
// above 16px when the surface dimension goes above 512px.
// TODO: These could be exposed as context options or surface options, and we may want to have
// different strategies in place for a base device vs. a layer's device.
static constexpr int kGridCellSize = 16;
static constexpr int kMaxBruteForceN = 64;
static constexpr int kMaxGridSize = 32;

Device::Device(Recorder* recorder, sk_sp<DrawContext> dc)
        : SkDevice(dc->imageInfo(), dc->surfaceProps())
        , fRecorder(recorder)
        , fDC(std::move(dc))
        , fClip(this)
        , fColorDepthBoundsManager(std::make_unique<HybridBoundsManager>(
                  fDC->imageInfo().dimensions(), kGridCellSize, kMaxBruteForceN, kMaxGridSize))
        , fDisjointStencilSet(std::make_unique<IntersectionTreeSet>())
        , fCachedLocalToDevice(SkM44())
        , fCurrentDepth(DrawOrder::kClearDepth)
        , fSubRunControl(recorder->priv().caps()->getSubRunControl(
                fDC->surfaceProps().isUseDeviceIndependentFonts())) {
    SkASSERT(SkToBool(fDC) && SkToBool(fRecorder));
    if (fDC->target()->textureInfo().numSamples() > 1) {
        // Target is inherently multisampled
        fMSAASupported = true;
    } else if (fRecorder->priv().caps()->defaultMSAASamplesCount() > 1) {
        if (fRecorder->priv().caps()->msaaRenderToSingleSampledSupport()) {
            // Backend-managed MSAA is supported
            fMSAASupported = true;
        } else {
            // Graphite-managed MSAA is supported
            fMSAASupported = fRecorder->priv().caps()->isSampleCountSupported(
                    TextureInfoPriv::ViewFormat(fDC->target()->textureInfo()),
                    fRecorder->priv().caps()->defaultMSAASamplesCount());
        }
    }
}

Device::~Device() {
    // The Device should have been marked immutable before it's destroyed, or the Recorder was the
    // last holder of a reference to it and de-registered the device as part of its cleanup.
    // However, if the Device was not registered with the recorder (i.e. a scratch device) we don't
    // require that its recorder be adandoned. Scratch devices must either have been marked
    // immutable or be destroyed before the recorder has been snapped.
    SkASSERT(!fRecorder || fScopedRecordingID != 0);
#if defined(SK_DEBUG)
    if (fScopedRecordingID != 0 && fRecorder) {
        SkASSERT(fScopedRecordingID == fRecorder->priv().nextRecordingID());
    }
    // else it wasn't a scratch device, or it was a scratch device that was marked immutable so its
    // lifetime was validated when setImmutable() was called.
#endif
}

void Device::setImmutable() {
    if (fRecorder) {
        // Push any pending work to the Recorder now. setImmutable() is only called by the
        // destructor of a client-owned Surface, or explicitly in layer/filtering workflows. In
        // both cases this is restricted to the Recorder's thread. This is in contrast to ~Device(),
        // which might be called from another thread if it was linked to an Image used in multiple
        // recorders.
        this->flushPendingWork(/*drawContext=*/nullptr);
        fRecorder->deregisterDevice(this);
        // Abandoning the recorder ensures that there are no further operations that can be recorded
        // and is relied on by Image::notifyInUse() to detect when it can unlink from a Device.
        this->abandonRecorder();
    }
}

const Transform& Device::localToDeviceTransform() {
    if (this->checkLocalToDeviceDirty()) {
        fCachedLocalToDevice = Transform{this->localToDevice44()};
    }
    return fCachedLocalToDevice;
}

SkStrikeDeviceInfo Device::strikeDeviceInfo() const {
    return {this->surfaceProps(), this->scalerContextFlags(), &fSubRunControl};
}

sk_sp<SkDevice> Device::createDevice(const CreateInfo& info, const SkPaint*) {
    // TODO: Inspect the paint and create info to determine if there's anything that has to be
    // modified to support inline subpasses.
    SkSurfaceProps props =
        this->surfaceProps().cloneWithPixelGeometry(info.fPixelGeometry);

    // Skia's convention is to only clear a device if it is non-opaque.
    LoadOp initialLoadOp = info.fInfo.isOpaque() ? LoadOp::kDiscard : LoadOp::kClear;

    std::string label = this->target()->label();
    if (label.empty()) {
        label = "ChildDevice";
    } else {
        label += "_ChildDevice";
    }

    return Make(fRecorder,
                info.fInfo,
                skgpu::Budgeted::kYes,
                Mipmapped::kNo,
                SkBackingFit::kApprox,
                props,
                initialLoadOp,
                label);
}

sk_sp<SkSurface> Device::makeSurface(const SkImageInfo& ii, const SkSurfaceProps& props) {
    return SkSurfaces::RenderTarget(fRecorder, ii, Mipmapped::kNo, &props);
}

// Although we have a drawContext here, we pass a nullptr to both flushPendingWork and Image::Copy
// so that tasks end up on the root task list.
sk_sp<Image> Device::makeImageCopy(const SkIRect& subset,
                                   Budgeted budgeted,
                                   Mipmapped mipmapped,
                                   SkBackingFit backingFit) {
    ASSERT_SINGLE_OWNER
    this->flushPendingWork(/*drawContext=*/nullptr);

    const SkColorInfo& colorInfo = this->imageInfo().colorInfo();
    TextureProxyView srcView = this->readSurfaceView();
    if (!srcView) {
        // readSurfaceView() returns an empty view when the target is not texturable. Create an
        // equivalent view for the blitting operation.
        Swizzle readSwizzle = fRecorder->priv().caps()->getReadSwizzle(
                colorInfo.colorType(), this->target()->textureInfo());
        srcView = {sk_ref_sp(this->target()), readSwizzle};
    }
    std::string label = this->target()->label();
    if (label.empty()) {
        label = "CopyDeviceTexture";
    } else {
        label += "_DeviceCopy";
    }

    return Image::Copy(fRecorder, /*drawContext=*/nullptr, srcView, colorInfo, subset, budgeted,
                       mipmapped, backingFit, label);
}

bool Device::onReadPixels(const SkPixmap& pm, int srcX, int srcY) {
#if defined(GPU_TEST_UTILS)
    // This testing-only function should only be called before the Device has detached from its
    // Recorder, since it's accessed via the test-held Surface.
    ASSERT_SINGLE_OWNER
    if (Context* context = fRecorder->priv().context()) {
        // Add all previous commands generated to the command buffer.
        // If the client snaps later they'll only get post-read commands in their Recording,
        // but since they're doing a readPixels in the middle that shouldn't be unexpected.
        std::unique_ptr<Recording> recording = fRecorder->snap();
        if (!recording) {
            return false;
        }
        InsertRecordingInfo info;
        info.fRecording = recording.get();
        if (!context->insertRecording(info)) {
            return false;
        }
        return context->priv().readPixels(pm, fDC->target(), this->imageInfo(), srcX, srcY);
    }
#endif
    // We have no access to a context to do a read pixels here.
    return false;
}

bool Device::onWritePixels(const SkPixmap& src, int x, int y) {
    ASSERT_SINGLE_OWNER
    // TODO: we may need to share this in a more central place to handle uploads
    // to backend textures

    const TextureProxy* target = fDC->target();

    // TODO: add mipmap support for createBackendTexture

    if (src.colorType() == kUnknown_SkColorType) {
        return false;
    }

    // If one alpha type is unknown and the other isn't, it's too underspecified.
    if ((src.alphaType() == kUnknown_SkAlphaType) !=
        (this->imageInfo().alphaType() == kUnknown_SkAlphaType)) {
        return false;
    }

    // TODO: canvas2DFastPath?

    if (!fRecorder->priv().caps()->supportsWritePixels(target->textureInfo())) {
        auto image = SkImages::RasterFromPixmap(src, nullptr, nullptr);
        image = SkImages::TextureFromImage(fRecorder, image.get());
        if (!image) {
            return false;
        }

        SkPaint paint;
        paint.setBlendMode(SkBlendMode::kSrc);
        this->drawImageRect(image.get(),
                            /*src=*/nullptr,
                            SkRect::MakeXYWH(x, y, src.width(), src.height()),
                            SkFilterMode::kNearest,
                            paint,
                            SkCanvas::kFast_SrcRectConstraint);
        return true;
    }

    // TODO: check for flips and either handle here or pass info to UploadTask

    // Determine rect to copy
    SkIRect dstRect = SkIRect::MakePtSize({x, y}, src.dimensions());
    if (!target->isFullyLazy() && !dstRect.intersect(SkIRect::MakeSize(target->dimensions()))) {
        return false;
    }

    // Set up copy location
    const void* addr = src.addr(dstRect.fLeft - x, dstRect.fTop - y);
    std::vector<MipLevel> levels;
    levels.push_back({addr, src.rowBytes()});

    // The writePixels() still respects painter's order, so flush everything to tasks before this
    // recording the upload for the pixel data.
    this->internalFlush();
    // The new upload will be executed before any new draws are recorded and also ensures that
    // the next call to flushDeviceToRecorder() will produce a non-null DrawTask. If this Device's
    // target is mipmapped, mipmap generation tasks will be added automatically at that point.
    const UploadSource uploadSource = UploadSource::Make(fRecorder->priv().caps(),
                                                         *fDC->refTarget(),
                                                         src.info().colorInfo(),
                                                         this->imageInfo().colorInfo(),
                                                         levels,
                                                         dstRect);
    if (!uploadSource.isValid()) {
        return false;
    }
    return fDC->recordUpload(fRecorder,
                             fDC->refTarget(),
                             src.info().colorInfo(),
                             this->imageInfo().colorInfo(),
                             uploadSource,
                             dstRect,
                             nullptr);
}


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

bool Device::isClipAntiAliased() const {
    // All clips are AA'ed unless it's wide-open, empty, or a device-rect with integer coordinates
    ClipStack::ClipState type = fClip.clipState();
    if (type == ClipStack::ClipState::kWideOpen || type == ClipStack::ClipState::kEmpty) {
        return false;
    } else if (type == ClipStack::ClipState::kDeviceRect) {
        const ClipStack::Element rect = *fClip.begin();
        SkASSERT(rect.fShape.isRect() && rect.fLocalToDevice.type() == Transform::Type::kIdentity);
        return rect.fShape.rect() != rect.fShape.rect().makeRoundOut();
    } else {
        return true;
    }
}

SkIRect Device::devClipBounds() const {
    return rect_to_pixelbounds(fClip.conservativeBounds());
}

// TODO: This is easy enough to support, but do we still need this API in Skia at all?
void Device::android_utils_clipAsRgn(SkRegion* region) const {
    SkIRect bounds = this->devClipBounds();
    // Assume wide open and then perform intersect/difference operations reducing the region
    region->setRect(bounds);
    const SkRegion deviceBounds(bounds);
    for (const ClipStack::Element& e : fClip) {
        SkRegion tmp;
        if (e.fShape.isRect() && e.fLocalToDevice.type() == Transform::Type::kIdentity) {
            tmp.setRect(rect_to_pixelbounds(e.fShape.rect()));
        } else {
            SkPath tmpPath = e.fShape.asPath().makeTransform(e.fLocalToDevice);
            tmp.setPath(tmpPath, deviceBounds);
        }

        region->op(tmp, (SkRegion::Op) e.fOp);
    }
}

void Device::clipRect(const SkRect& rect, SkClipOp op, bool aa) {
    SkASSERT(op == SkClipOp::kIntersect || op == SkClipOp::kDifference);
    auto snapping = aa ? ClipStack::PixelSnapping::kNo : ClipStack::PixelSnapping::kYes;
    fClip.clipShape(this->localToDeviceTransform(), Shape{rect}, op, snapping);
}

void Device::clipRRect(const SkRRect& rrect, SkClipOp op, bool aa) {
    SkASSERT(op == SkClipOp::kIntersect || op == SkClipOp::kDifference);
    auto snapping = aa ? ClipStack::PixelSnapping::kNo : ClipStack::PixelSnapping::kYes;
    fClip.clipShape(this->localToDeviceTransform(), Shape{rrect}, op, snapping);
}

void Device::clipPath(const SkPath& path, SkClipOp op, bool aa) {
    SkASSERT(op == SkClipOp::kIntersect || op == SkClipOp::kDifference);
    // TODO: Ensure all path inspection is handled here or in SkCanvas, and that non-AA rects as
    // paths are routed appropriately.
    // TODO: Must also detect paths that are lines so the clip stack can be set to empty
    fClip.clipShape(this->localToDeviceTransform(), Shape{path}, op);
}

void Device::onClipShader(sk_sp<SkShader> shader) {
    fClip.clipShader(std::move(shader));
}

// TODO: Is clipRegion() on the deprecation chopping block. If not it should be...
void Device::clipRegion(const SkRegion& globalRgn, SkClipOp op) {
    SkASSERT(op == SkClipOp::kIntersect || op == SkClipOp::kDifference);

    Transform globalToDevice{this->globalToDevice()};

    if (globalRgn.isEmpty()) {
        fClip.clipShape(globalToDevice, Shape{}, op);
    } else if (globalRgn.isRect()) {
        fClip.clipShape(globalToDevice, Shape{SkRect::Make(globalRgn.getBounds())}, op,
                        ClipStack::PixelSnapping::kYes);
    } else {
        // TODO: Can we just iterate the region and do non-AA rects for each chunk?
        SkPath path = globalRgn.getBoundaryPath();
        fClip.clipShape(globalToDevice, Shape{path}, op);
    }
}

void Device::replaceClip(const SkIRect& rect) {
    // ReplaceClip() is currently not intended to be supported in Graphite since it's only used
    // for emulating legacy clip ops in Android Framework, and apps/devices that require that
    // should not use Graphite. However, if it needs to be supported, we could probably implement
    // it by:
    //  1. Flush all pending clip element depth draws.
    //  2. Draw a fullscreen rect to the depth attachment using a Z value greater than what's
    //     been used so far.
    //  3. Make sure all future "unclipped" draws use this Z value instead of 0 so they aren't
    //     sorted before the depth reset.
    //  4. Make sure all prior elements are inactive so they can't affect subsequent draws.
    //
    // For now, just ignore it.
}

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

void Device::drawPaint(const SkPaint& paint) {
    ASSERT_SINGLE_OWNER

    Shape inverseFill; // defaults to empty
    inverseFill.setInverted(true);
    // An empty shape with an inverse fill completely floods the clip
    SkASSERT(inverseFill.isFloodFill());

    this->drawGeometry(this->localToDeviceTransform(),
                       Geometry(inverseFill),
                       paint,
                       DefaultFillStyle(),
                       DrawFlags::kIgnorePathEffect);
}

void Device::drawRect(const SkRect& r, const SkPaint& paint) {
    Rect rectToDraw(r);
    SkStrokeRec style(paint);
    if (!paint.isAntiAlias()) {
        // Graphite assumes everything is anti-aliased. In the case of axis-aligned non-aa requested
        // rectangles, we snap the local geometry to land on pixel boundaries to emulate non-aa.
        if (style.isFillStyle()) {
            rectToDraw = snap_rect_to_pixels(this->localToDeviceTransform(), rectToDraw);
        } else {
            const bool strokeAndFill = style.getStyle() == SkStrokeRec::kStrokeAndFill_Style;
            float strokeWidth = style.getWidth();
            rectToDraw = snap_rect_to_pixels(this->localToDeviceTransform(),
                                             rectToDraw, &strokeWidth);
            style.setStrokeStyle(strokeWidth, strokeAndFill);
        }
    }
    this->drawGeometry(this->localToDeviceTransform(), Geometry(Shape(rectToDraw)), paint, style);
}

void Device::drawVertices(const SkVertices* vertices, sk_sp<SkBlender> blender,
                          const SkPaint& paint, bool skipColorXform)  {
    this->drawGeometry(this->localToDeviceTransform(),
                       Geometry(sk_ref_sp(vertices)),
                       paint,
                       DefaultFillStyle(),
                       DrawFlags::kIgnorePathEffect,
                       std::move(blender),
                       skipColorXform);
}

bool Device::drawAsTiledImageRect(SkCanvas* canvas,
                                  const SkImage* image,
                                  const SkRect* src,
                                  const SkRect& dst,
                                  const SkSamplingOptions& sampling,
                                  const SkPaint& paint,
                                  SkCanvas::SrcRectConstraint constraint) {
    auto recorder = canvas->recorder();
    if (!recorder) {
        return false;
    }
    SkASSERT(src);

    // For Graphite this is a pretty loose heuristic. The Recorder-local cache size (relative
    // to the large image's size) is used as a proxy for how conservative we should be when
    // allocating tiles. Since the tiles will actually be owned by the client (via an
    // ImageProvider) they won't actually add any memory pressure directly to Graphite.
    size_t cacheSize = recorder->priv().getResourceCacheLimit();
    size_t maxTextureSize = recorder->priv().caps()->maxTextureSize();

#if defined(GPU_TEST_UTILS)
    if (gOverrideMaxTextureSizeGraphite) {
        maxTextureSize = gOverrideMaxTextureSizeGraphite;
    }
    gNumTilesDrawnGraphite.store(0, std::memory_order_relaxed);
#endif

    // DrawAsTiledImageRect produces per-edge AA quads, which do not participate in non-AA pixel
    // snapping emulation. To match an un-tiled drawImageRect, round the src and dst geometry
    // before any tiling occurs.
    SkRect finalSrc = *src;
    SkRect finalDst = dst;
    if (!paint.isAntiAlias()) {
        snap_src_and_dst_rect_to_pixels(this->localToDeviceTransform(),
                                        &finalSrc, &finalDst);
    }

    [[maybe_unused]] auto [wasTiled, numTiles] =
            skgpu::TiledTextureUtils::DrawAsTiledImageRect(canvas,
                                                           image,
                                                           finalSrc,
                                                           finalDst,
                                                           SkCanvas::kAll_QuadAAFlags,
                                                           sampling,
                                                           &paint,
                                                           constraint,
                                                           /* sharpenMM= */ true,
                                                           cacheSize,
                                                           maxTextureSize);
#if defined(GPU_TEST_UTILS)
    gNumTilesDrawnGraphite.store(numTiles, std::memory_order_relaxed);
#endif
    return wasTiled;
}

void Device::drawOval(const SkRect& oval, const SkPaint& paint) {
    if (paint.getPathEffect()) {
        // Dashing requires that the oval path starts on the right side and travels clockwise. This
        // is the default for the SkPath::Oval constructor, as used by SkBitmapDevice.
        this->drawGeometry(this->localToDeviceTransform(), Geometry(Shape(SkPath::Oval(oval))),
                           paint, SkStrokeRec(paint));
    } else {
        // TODO: This has wasted effort from the SkCanvas level since it instead converts rrects
        // that happen to be ovals into this, only for us to go right back to rrect.
        this->drawRRect(SkRRect::MakeOval(oval), paint);
    }
}

void Device::drawArc(const SkArc& arc, const SkPaint& paint) {
    // For sweeps >= 360°, simple fills and simple strokes without the center point or square caps
    // are ovals. Culling these here simplifies the path processing in Shape.
    if (!paint.getPathEffect() &&
        SkScalarAbs(arc.sweepAngle()) >= 360.f &&
        (paint.getStyle() == SkPaint::kFill_Style ||
         (paint.getStyle() == SkPaint::kStroke_Style &&
          // square caps can stick out from the shape so we can't do this with an rrect draw
          paint.getStrokeCap() != SkPaint::kSquare_Cap &&
          // wedge cases with strokes will draw lines to the center
          !arc.isWedge()))) {
        this->drawRRect(SkRRect::MakeOval(arc.oval()), paint);
    } else {
        this->drawGeometry(this->localToDeviceTransform(), Geometry(Shape(arc)),
                           paint, SkStrokeRec(paint));
    }
}

void Device::drawRRect(const SkRRect& rr, const SkPaint& paint) {
    Shape rrectToDraw;
    SkStrokeRec style(paint);

    if (paint.isAntiAlias()) {
        rrectToDraw.setRRect(rr);
    } else {
        // Snap the horizontal and vertical edges of the rounded rectangle to pixel edges to match
        // the behavior of drawRect(rr.bounds()), to partially emulate non-AA rendering while
        // preserving the anti-aliasing of the curved corners.
        Rect snappedBounds;
        if (style.isFillStyle()) {
            snappedBounds = snap_rect_to_pixels(this->localToDeviceTransform(), rr.rect());
        } else {
            const bool strokeAndFill = style.getStyle() == SkStrokeRec::kStrokeAndFill_Style;
            float strokeWidth = style.getWidth();
            snappedBounds = snap_rect_to_pixels(this->localToDeviceTransform(),
                                                rr.rect(), &strokeWidth);
            style.setStrokeStyle(strokeWidth, strokeAndFill);
        }

        SkRRect snappedRRect;
        snappedRRect.setRectRadii(snappedBounds.asSkRect(), rr.radii().data());
        rrectToDraw.setRRect(snappedRRect);
    }

    this->drawGeometry(this->localToDeviceTransform(), Geometry(rrectToDraw), paint, style);
}

void Device::drawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint& paint) {
#if !defined(SK_DISABLE_CLIP_DRAW_GEOMETRIC_INTERSECTION)
    // If there's a path effect or inverse fill, fall back to path rendering
    if (paint.getPathEffect() || paint.getStyle() != SkPaint::kFill_Style) {
        this->SkDevice::drawDRRect(outer, inner, paint);
        return;
    }

    // This holds the positive insets from `outer` to `inner`
    Rect strokeRect{outer.rect()};
    skvx::float4 gap = Rect(inner.rect()).vals() - strokeRect.vals();
    const float tolerance = 0.001f * this->localToDeviceTransform().localAARadius(strokeRect);
    const float strokeWidth = gap[0];
    if (!all((gap > tolerance) & (abs(gap - strokeWidth) <= tolerance))) {
        // Either not approximately equal insets on all sides, or it would create a hairline stroke
        // which is something that should be requested via paint style.
        //
        // But we can try subtracting inner from outer if they are both rectangular to see if it
        // leaves a valid filled rect.
        SkRect diff;
        if (outer.isRect() && inner.isRect() &&
            SkRectPriv::Subtract(outer.rect(), inner.rect(), &diff)) {
            this->drawRect(diff, paint);
            return;
        }

        // Fall through to the draw+clip handling
    } else {
        // The shape is possibly expressible as a stroked [r]rect.
        const float strokeRadius = 0.5f * strokeWidth;
        strokeRect.inset(strokeRadius);

        SkPaint strokePaint = paint;
        strokePaint.setStroke(true);
        strokePaint.setStrokeWidth(strokeWidth);
        strokePaint.setStrokeCap(SkPaint::kButt_Cap);
        strokePaint.setStrokeMiter(4.f); // large enough to not trigger bevels for 90 degree corners

        // Check the corners to see if they are equivalent to a stroked round rect. If `outer` has
        // rounded corners, they must be circular and be at least `strokeRadius`. An outer corner
        // can have a 0 radius if we assume a miter join style, but if an outer corner is exactly
        // the stroke radius then we have to use a round join style. The matching corners of `inner`
        // must also be rounded, and be exactly strokeWidth less, or they must be 0 if the
        // difference would be negative.
        int validCorners = 0;
        int rectCorners = 0;
        SkVector strokeCorners[4];
        std::optional<SkPaint::Join> requiredJoin;
        for (int i = 0; i < 4; ++i) {
            SkVector outerCornerRadii = outer.radii((SkRRect::Corner) i);
            SkVector innerCornerRadii = inner.radii((SkRRect::Corner) i);

            float strokeCorner;
            if (!SkScalarNearlyEqual(outerCornerRadii.fX, outerCornerRadii.fY, tolerance)) {
                // Not circular; a stroked ellipse is not just a larger ellipse
                break;
            } else if (SkScalarNearlyZero(outerCornerRadii.fX, tolerance)) {
                // A rectangular outer corner requires miter joins
                if (requiredJoin.has_value() && *requiredJoin != SkPaint::kMiter_Join) {
                    break;
                }
                requiredJoin = SkPaint::kMiter_Join;
                strokeCorner = 0.f;
                rectCorners++;
            } else {
                strokeCorner = outerCornerRadii.fX - strokeRadius;
                if (strokeCorner < -tolerance) {
                    // Corner is rounded but less than the stroke radius, which isn't representable
                    break;
                } else if (strokeCorner <= tolerance) {
                    // Corner is rounded to the stroke radius, which can only be represented as an
                    // underlying rect corner and round join
                    if (requiredJoin.has_value() && *requiredJoin != SkPaint::kRound_Join) {
                        break;
                    }
                    requiredJoin = SkPaint::kRound_Join;
                    strokeCorner = 0.f;
                    rectCorners++;
                }
            }

            float expectedInnerRadius = std::max(0.f, strokeCorner - strokeRadius);
            if (!SkScalarNearlyEqual(innerCornerRadii.fX, expectedInnerRadius) ||
                !SkScalarNearlyEqual(innerCornerRadii.fY, expectedInnerRadius)) {
                // Inner corner doesn't match expectation
                break;
            }

            strokeCorners[i] = {strokeCorner, strokeCorner};
            validCorners++;
        }

        if (validCorners == 4) {
            strokePaint.setStrokeJoin(requiredJoin.value_or(SkPaint::kRound_Join));
            if (rectCorners == 4) {
                this->drawRect(strokeRect.asSkRect(), strokePaint);
            } else {
                SkRRect strokeRRect;
                strokeRRect.setRectRadii(strokeRect.asSkRect(), strokeCorners);
                this->drawRRect(strokeRRect, strokePaint);
            }
            return;
        }
        // Otherwise fall through to draw+clip handling
    }

    // To avoid path rendering, treat DRRects as a drawRRect(outer) with a clipRRect(inner, kDiff)
    fClip.save();
    fClip.clipShape(this->localToDeviceTransform(),
                    inner.isRect() ? Shape{inner.rect()} : Shape{inner},
                    SkClipOp::kDifference,
                    paint.isAntiAlias() ? ClipStack::PixelSnapping::kNo
                                        : ClipStack::PixelSnapping::kYes);
    if (outer.isRect()) {
        this->drawRect(outer.rect(), paint);
    } else {
        this->drawRRect(outer, paint);
    }
    fClip.restore();
#else
    this->SkDevice::drawDRRect(outer, inner, paint);
#endif
}

void Device::drawPath(const SkPath& path, const SkPaint& paint, bool pathIsMutable) {
    // Alternatively, we could move this analysis to SkCanvas. Also, we could consider applying the
    // path effect, being careful about starting point and direction.
    if (!paint.getPathEffect() && !path.isInverseFillType()) {
        SkPoint linePts[2];
        if (path.isLine(linePts)) {
            // A line has zero area, so stroke and stroke-and-fill are equivalent
            if (paint.getStyle() != SkPaint::kFill_Style) {
                this->drawPoints(SkCanvas::kLines_PointMode, linePts, paint);
            } // and if it's fill, nothing is drawn
            return;
        }
        if (SkRect oval; path.isOval(&oval)) {
            this->drawOval(oval, paint);
            return;
        }
        if (SkRRect rrect; path.isRRect(&rrect)) {
            this->drawRRect(rrect, paint);
            return;
        }
        // For rects, if the path is not explicitly closed and the paint style is stroked then it
        // represents a rectangle with only 3 sides rasterized (and with any caps). If it's filled
        // or is closed+stroked, then the path renders identically to the rectangle.
        bool isClosed = false;
        if (SkRect rect; path.isRect(&rect, &isClosed) &&
            (paint.getStyle() == SkPaint::kFill_Style || isClosed)) {
            this->drawRect(rect, paint);
            return;
        }
#if !defined(SK_DISABLE_CLIP_DRAW_GEOMETRIC_INTERSECTION)
        // Detect filled nested rect contours
        SkRect rects[2];
        SkPathDirection dirs[2];
        if (paint.getStyle() == SkPaint::kFill_Style &&
            SkPathPriv::IsNestedFillRects(path, rects, dirs)) {
            // For winding fills with contours going the same direction, there isn't any cutout
            if (path.getFillType() == SkPathFillType::kWinding && dirs[0] == dirs[1]) {
                this->drawRect(rects[0], paint);
                return;
            } else {
                // The inner is cut out from the outer rect. Delegate to drawDRRect.
                this->drawDRRect(SkRRect::MakeRect(rects[0]), SkRRect::MakeRect(rects[1]), paint);
                return;
            }
        }
#endif
    }
    this->drawGeometry(this->localToDeviceTransform(), Geometry(Shape(path)),
                       paint, SkStrokeRec(paint));
}

void Device::drawPoints(SkCanvas::PointMode mode, SkSpan<const SkPoint> points,
                        const SkPaint& paint) {
    if (points.empty()) {
        return;
    }
    size_t count = points.size();

    SkStrokeRec stroke(paint, SkPaint::kStroke_Style);
    size_t next = 0;
    if (mode == SkCanvas::kPoints_PointMode) {
        // Treat kPoints mode as stroking zero-length path segments, which produce caps so that
        // both hairlines and round vs. square geometry are handled entirely on the GPU.
        // TODO: SkCanvas should probably do the butt to square cap correction.
        if (paint.getStrokeCap() == SkPaint::kButt_Cap) {
            stroke.setStrokeParams(SkPaint::kSquare_Cap,
                                   paint.getStrokeJoin(),
                                   paint.getStrokeMiter());
        }
    } else {
        next = 1;
        count--;
    }

    size_t inc = mode == SkCanvas::kLines_PointMode ? 2 : 1;
    for (size_t i = 0; i < count; i += inc) {
        this->drawGeometry(this->localToDeviceTransform(),
                           Geometry(Shape(points[i], points[i + next])),
                           paint, stroke);
    }
}

void Device::drawEdgeAAQuad(const SkRect& rect,
                            const SkPoint clip[4],
                            SkCanvas::QuadAAFlags aaFlags,
                            const SkColor4f& color,
                            SkBlendMode mode) {
    SkPaint solidColorPaint;
    solidColorPaint.setColor4f(color, /*colorSpace=*/nullptr);
    solidColorPaint.setBlendMode(mode);

    // NOTE: We do not snap edge AA quads that are fully non-AA because we need their edges to seam
    // with quads that have mixed edge flags (so both need to match the GPU rasterization, not our
    // CPU rounding).
    auto flags = SkEnumBitMask<EdgeAAQuad::Flags>(static_cast<EdgeAAQuad::Flags>(aaFlags));
    EdgeAAQuad quad = clip ? EdgeAAQuad(clip, flags) : EdgeAAQuad(rect, flags);
    this->drawGeometry(this->localToDeviceTransform(),
                       Geometry(quad),
                       solidColorPaint,
                       DefaultFillStyle(),
                       DrawFlags::kIgnorePathEffect);
}

void Device::drawEdgeAAImageSet(const SkCanvas::ImageSetEntry set[], int count,
                                const SkPoint dstClips[], const SkMatrix preViewMatrices[],
                                const SkSamplingOptions& sampling, const SkPaint& paint,
                                SkCanvas::SrcRectConstraint constraint) {
    SkASSERT(count > 0);

    SkPaint paintWithShader(paint);
    int dstClipIndex = 0;
    for (int i = 0; i < count; ++i) {
        // If the entry is clipped by 'dstClips', that must be provided
        SkASSERT(!set[i].fHasClip || dstClips);
        // Similarly, if it has an extra transform, those must be provided
        SkASSERT(set[i].fMatrixIndex < 0 || preViewMatrices);

        auto [ imageToDraw, newSampling ] =
                skgpu::graphite::GetGraphiteBacked(this->recorder(), set[i].fImage.get(), sampling);
        if (!imageToDraw) {
            SKGPU_LOG_W("Device::drawImageRect: Creation of Graphite-backed image failed");
            return;
        }

        // TODO: Produce an image shading paint key and data directly without having to reconstruct
        // the equivalent SkPaint for each entry. Reuse the key and data between entries if possible
        paintWithShader.setShader(paint.refShader());
        paintWithShader.setAlphaf(paint.getAlphaf() * set[i].fAlpha);
        SkRect dst = SkModifyPaintAndDstForDrawImageRect(
                    imageToDraw.get(), newSampling, set[i].fSrcRect, set[i].fDstRect,
                    constraint == SkCanvas::kStrict_SrcRectConstraint,
                    &paintWithShader);
        if (dst.isEmpty()) {
            return;
        }

        // NOTE: See drawEdgeAAQuad for details, we do not snap non-AA quads.
        auto flags =
                SkEnumBitMask<EdgeAAQuad::Flags>(static_cast<EdgeAAQuad::Flags>(set[i].fAAFlags));
        EdgeAAQuad quad = set[i].fHasClip ? EdgeAAQuad(dstClips + dstClipIndex, flags)
                                          : EdgeAAQuad(dst, flags);

        // TODO: Calling drawGeometry() for each entry re-evaluates the clip stack every time, which
        // is consistent with Ganesh's behavior. It also matches the behavior if edge-AA images were
        // submitted one at a time by SkiaRenderer (a nice client simplification). However, we
        // should explore the performance trade off with doing one bulk evaluation for the whole set
        if (set[i].fMatrixIndex < 0) {
            this->drawGeometry(this->localToDeviceTransform(),
                               Geometry(quad),
                               paintWithShader,
                               DefaultFillStyle(),
                               DrawFlags::kIgnorePathEffect);
        } else {
            SkM44 xtraTransform(preViewMatrices[set[i].fMatrixIndex]);
            this->drawGeometry(this->localToDeviceTransform().concat(xtraTransform),
                               Geometry(quad),
                               paintWithShader,
                               DefaultFillStyle(),
                               DrawFlags::kIgnorePathEffect);
        }

        dstClipIndex += 4 * set[i].fHasClip;
    }
}

void Device::drawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst,
                           const SkSamplingOptions& sampling, const SkPaint& paint,
                           SkCanvas::SrcRectConstraint constraint) {
    SkCanvas::ImageSetEntry single{sk_ref_sp(image),
                                   src ? *src : SkRect::Make(image->bounds()),
                                   dst,
                                   /*alpha=*/1.f,
                                   SkCanvas::kAll_QuadAAFlags};
    // While this delegates to drawEdgeAAImageSet() for the image shading logic, semantically a
    // drawImageRect()'s non-AA behavior should match that of drawRect() so we snap dst (and update
    // src to match) if needed before hand.
    if (!paint.isAntiAlias()) {
        snap_src_and_dst_rect_to_pixels(this->localToDeviceTransform(),
                                        &single.fSrcRect, &single.fDstRect);
    }
    this->drawEdgeAAImageSet(&single, 1, nullptr, nullptr, sampling, paint, constraint);
}

sktext::gpu::AtlasDrawDelegate Device::atlasDelegate() {
    return [&](const sktext::gpu::AtlasSubRun* subRun,
               SkPoint drawOrigin,
               const SkPaint& paint,
               sk_sp<SkRefCnt> subRunStorage,
               sktext::gpu::RendererData rendererData) {
        this->drawAtlasSubRun(subRun, drawOrigin, paint, std::move(subRunStorage), rendererData);
    };
}

void Device::onDrawGlyphRunList(SkCanvas* canvas,
                                const sktext::GlyphRunList& glyphRunList,
                                const SkPaint& paint) {
    ASSERT_SINGLE_OWNER
    fRecorder->priv().textBlobCache()->drawGlyphRunList(canvas,
                                                        this->localToDevice(),
                                                        glyphRunList,
                                                        paint,
                                                        this->strikeDeviceInfo(),
                                                        this->atlasDelegate());
}

void Device::drawAtlasSubRun(const sktext::gpu::AtlasSubRun* subRun,
                             SkPoint drawOrigin,
                             const SkPaint& paint,
                             sk_sp<SkRefCnt> subRunStorage,
                             sktext::gpu::RendererData rendererData) {
    ASSERT_SINGLE_OWNER

    const int subRunEnd = subRun->glyphCount();
    auto regenerateDelegate = [&](sktext::gpu::GlyphVector* glyphs,
                                  int begin,
                                  int end,
                                  skgpu::MaskFormat maskFormat,
                                  int padding) {
        return glyphs->regenerateAtlasForGraphite(begin, end, maskFormat, padding, fRecorder);
    };
    for (int subRunCursor = 0; subRunCursor < subRunEnd;) {
        // For the remainder of the run, add any atlas uploads to the Recorder's TextAtlasManager
        auto[ok, glyphsRegenerated] = subRun->regenerateAtlas(subRunCursor, subRunEnd,
                                                              regenerateDelegate);
        // There was a problem allocating the glyph in the atlas. Bail.
        if (!ok) {
            return;
        }
        if (glyphsRegenerated) {
            auto [bounds, localToDevice] = subRun->vertexFiller().boundsAndDeviceMatrix(
                                                   this->localToDeviceTransform(), drawOrigin);
            SkPaint subRunPaint = paint;
            // For color emoji, shaders don't affect the final color
            if (subRun->maskFormat() == skgpu::MaskFormat::kARGB) {
                subRunPaint.setShader(nullptr);
            }

            bool useGammaCorrectDistanceTable =
                    this->imageInfo().colorSpace() &&
                    this->imageInfo().colorSpace()->gammaIsLinear();
            this->drawGeometry(localToDevice,
                               Geometry(SubRunData(subRun,
                                                   subRunStorage,
                                                   bounds,
                                                   this->localToDeviceTransform().inverse(),
                                                   subRunCursor,
                                                   glyphsRegenerated,
                                                   SkPaintPriv::ComputeLuminanceColor(subRunPaint),
                                                   useGammaCorrectDistanceTable,
                                                   this->surfaceProps().pixelGeometry(),
                                                   fRecorder,
                                                   rendererData)),
                               subRunPaint,
                               DefaultFillStyle(),
                               DrawFlags::kIgnorePathEffect,
                               SkBlender::Mode(SkBlendMode::kDstIn));
        }
        subRunCursor += glyphsRegenerated;

        if (subRunCursor < subRunEnd) {
            // Flush if not all the glyphs are handled because the atlas is out of space.
            // We flush every Device because the glyphs that are being flushed/referenced are not
            // necessarily specific to this Device. This addresses both multiple SkSurfaces within
            // a Recorder, and nested layers.
            TRACE_EVENT_INSTANT0("skia.gpu", "Glyph atlas full", TRACE_EVENT_SCOPE_NAME_THREAD);
            fRecorder->priv().flushTrackedDevices();
        }
    }
}

void Device::drawGeometry(const Transform& localToDevice,
                          Geometry&& geometry,
                          const SkPaint& paint,
                          const SkStrokeRec& style,
                          SkEnumBitMask<DrawFlags> flags,
                          sk_sp<SkBlender> primitiveBlender,
                          bool skipColorXform) {
    ASSERT_SINGLE_OWNER

    if (!localToDevice.valid()) {
        // If the transform is not invertible or not finite then drawing isn't well defined.
        SKGPU_LOG_W("Skipping draw with non-invertible/non-finite transform.");
        return;
    }

    // Heavy weight paint options like path effects, mask filters, and stroke-and-fill style are
    // applied on the CPU by generating a new shape and recursing on drawGeometry with updated flags
    if (!(flags & DrawFlags::kIgnorePathEffect) && paint.getPathEffect()) {
        // Apply the path effect before anything else, which if we are applying here, means that we
        // are dealing with a Shape. drawVertices (and a SkVertices geometry) should pass in
        // kIgnorePathEffect per SkCanvas spec. Text geometry also should pass in kIgnorePathEffect
        // because the path effect is applied per glyph by the SkStrikeSpec already.
        SkASSERT(geometry.isShape());

        // TODO: If asADash() returns true and the base path matches the dashing fast path, then
        // that should be detected now as well. Maybe add dashPath to Device so canvas can handle it
        SkStrokeRec newStyle = style;
        float maxScaleFactor = localToDevice.maxScaleFactor();
        if (localToDevice.type() == Transform::Type::kPerspective) {
            auto bounds = geometry.bounds();
            float tl = std::get<1>(localToDevice.scaleFactors({bounds.left(), bounds.top()}));
            float tr = std::get<1>(localToDevice.scaleFactors({bounds.right(), bounds.top()}));
            float br = std::get<1>(localToDevice.scaleFactors({bounds.right(), bounds.bot()}));
            float bl = std::get<1>(localToDevice.scaleFactors({bounds.left(), bounds.bot()}));
            maxScaleFactor = std::max(std::max(tl, tr), std::max(bl, br));
        }
        newStyle.setResScale(maxScaleFactor);
        SkPathBuilder builder;
        if (paint.getPathEffect()->filterPath(&builder, geometry.shape().asPath(), &newStyle,
                                              nullptr, localToDevice)) {
            SkPath dst = builder.detach();
            dst.setIsVolatile(true);
            // Recurse using the path and new style, while disabling downstream path effect handling
            this->drawGeometry(localToDevice, Geometry(Shape(dst)), paint, newStyle,
                               flags | DrawFlags::kIgnorePathEffect, std::move(primitiveBlender),
                               skipColorXform);
            return;
        } else {
            SKGPU_LOG_W("Path effect failed to apply, drawing original path.");
            this->drawGeometry(localToDevice, std::move(geometry), paint, style,
                               flags | DrawFlags::kIgnorePathEffect, std::move(primitiveBlender),
                               skipColorXform);
            return;
        }
    }

    // TODO: The tessellating and atlas path renderers haven't implemented perspective yet, so
    // transform to device space so we draw something approximately correct (barring local coord
    // issues).
    if (geometry.isShape() && localToDevice.type() == Transform::Type::kPerspective &&
        !is_simple_shape(geometry.shape(), style.getStyle())) {
        SkPath devicePath = geometry.shape().asPath().makeTransform(localToDevice.matrix().asM33());
        devicePath.setIsVolatile(true);
        this->drawGeometry(Transform::Identity(), Geometry(Shape(devicePath)), paint, style, flags,
                           std::move(primitiveBlender), skipColorXform);
        return;
    }

    // TODO: Manually snap pixels for rects, rrects, and lines if paint is non-AA (ideally also
    // consider snapping stroke width and/or adjusting geometry for hairlines). This pixel snapping
    // math should be consistent with how non-AA clip [r]rects are handled.

    // If we got here, then path effects should have been handled and the style should be fill or
    // stroke/hairline. Stroke-and-fill is not handled by DrawContext, but is emulated here by
    // drawing twice--one stroke and one fill--using the same depth value.
    SkASSERT(!SkToBool(paint.getPathEffect()) || (flags & DrawFlags::kIgnorePathEffect));

    // Calculate the clipped bounds of the draw and determine the clip elements that affect the
    // draw without updating the clip stack.
    ClipStack::ElementList clipElements;
    Clip clip = fClip.visitClipStackForDraw(localToDevice,
                                            &geometry,
                                            style,
                                            fMSAASupported,
                                            &clipElements);
    if (clip.isClippedOut()) {
        // Clipped out, so don't record anything.
        return;
    }

    // We assume that we will receive a renderer, or a PathAtlas. If it's a PathAtlas,
    // then we assume that the renderer chosen in PathAtlas::addShape() will have
    // single-channel coverage, require AA bounds outsetting, and have a single renderStep.
    // The clip's draw bounds are passed in for heuristics, so it's fine if it doesn't include the
    // AA outsetting we add for some analytic coverage renderers.
    auto [renderer, pathAtlas] = this->chooseRenderer(localToDevice,
                                                      geometry,
                                                      style,
                                                      clip.transformedShapeBounds(),
                                                      /*requireMSAA=*/false);
    if (!renderer && !pathAtlas) {
        SKGPU_LOG_W("Skipping draw with no supported renderer or PathAtlas.");
        return;
    }

    // Update the pixel bounds of the draw to include any outsets done by the renderer (or that
    // must be included in the pixels required when using an atlas). This is important so that
    // all bounds overlap checks take into account pixels touched by rasterization, even if the
    // calculated coverage for a pixel is 0.
    if (!renderer || renderer->outsetBoundsForAA()) {
        clip.outsetBoundsForAA();
    }

    // Figure out what dst color requirements we have, if any.
    const SkBlenderBase* blender = as_BB(paint.getBlender());
    const std::optional<SkBlendMode> blendMode = blender ? blender->asBlendMode()
                                                         : SkBlendMode::kSrcOver;

    // A primitive blender should be ignored if there is no primitive color to blend against.
    // Additionally, if a renderer emits a primitive color, then a null primitive blender should
    // be interpreted as SrcOver blending mode.
    if (!renderer || !renderer->emitsPrimitiveColor()) {
        primitiveBlender = nullptr;
    } else if (!SkToBool(primitiveBlender)) {
        primitiveBlender = SkBlender::Mode(SkBlendMode::kSrcOver);
    }

    Coverage rendererCoverage = renderer ? renderer->coverage()
                                         : Coverage::kSingleChannel;
    TextureFormat targetFormat = TextureInfoPriv::ViewFormat(fDC->target()->textureInfo());
    PaintParams shading{paint,
                        std::move(primitiveBlender),
                        clip.nonMSAAClip(),
                        sk_ref_sp(clip.shader()),
                        rendererCoverage,
                        targetFormat,
                        skipColorXform};
    if (clip.needsCoverage() && rendererCoverage == Coverage::kNone) {
        // Must upgrade to single channel coverage if the clip requires coverage;
        // but preserve LCD coverage if the Renderer uses that.
        rendererCoverage = Coverage::kSingleChannel;
    }
    bool dstReadRequired = !CanUseHardwareBlending(fRecorder->priv().caps(),
                                                   targetFormat,
                                                   blendMode,
                                                   rendererCoverage);
    const bool dependsOnDst = paint_depends_on_dst(shading) ||
                              clip.shader() || !clip.nonMSAAClip().isEmpty();

    // If we are unclipped, do not depend on the dst, and cover the target, then we can adjust
    // load ops of the renderpass to more optimally handle the draw (and avoid redundant clears).
    // NOTE: We skip this for fully-lazy render targets because the load ops may impact a larger
    // area than the Device's theoretical bounds.
    const bool overwritesAllPixels = !dependsOnDst &&
                                     geometry.isShape() &&
                                     geometry.shape().isFloodFill() &&
                                     !fDC->target()->isFullyLazy() &&
                                     clipElements.empty() &&
                                     clip.scissor().contains(this->bounds());
    if (overwritesAllPixels) {
        if (std::optional<SkColor4f> color = extract_paint_color(shading, fDC->colorInfo())) {
            // Fullscreen clear, so nothing has to be rendered at all
            fDC->clear(*color);
            return;
        } else {
            // This paint does not depend on the destination and covers the entire surface, so
            // discard everything previously recorded and proceed with the draw. However, if we are
            // here because of a paint with src-over blending that just happens to be opaque, the
            // discarded dst can still be accessed. For non-floating point formats, that is fine,
            // but float formats can have NaNs after a discard that cause blending to fail. To
            // avoid that scenario, we clear to a known value instead.
            if (shading.asFinalBlendMode() == SkBlendMode::kSrcOver &&
                TextureFormatIsFloatingPoint(
                        TextureInfoPriv::ViewFormat(fDC->target()->textureInfo()))) {
                fDC->clear(SkColors::kMagenta); // This color doesn't matter
            } else {
                fDC->discard();
            }
            // But then continue to render the flood fill with shading
        }
    }

    // Some shapes and styles combine multiple draws so the total render step count is split between
    // the main renderer and possibly a secondaryRenderer.
    SkStrokeRec::Style styleType = style.getStyle();
    const Renderer* secondaryRenderer = nullptr;
    Rect innerFillBounds = Rect::InfiniteInverted();
    if (renderer) {
        if (styleType == SkStrokeRec::kStrokeAndFill_Style) {
            // `renderer` covers the fill, `secondaryRenderer` covers the stroke
            secondaryRenderer = fRecorder->priv().rendererProvider()->tessellatedStrokes();
        } else if (style.isFillStyle() && renderer->useNonAAInnerFill() && !dependsOnDst) {
            // `renderer` opts into drawing a non-AA inner fill
            innerFillBounds = get_inner_bounds(geometry, localToDevice);
            if (!innerFillBounds.isEmptyNegativeOrNaN()) {
                secondaryRenderer = fRecorder->priv().rendererProvider()->nonAABounds();
            }
        }
    }
    const int numNewRenderSteps = (renderer ? renderer->numRenderSteps() : 1) +
                                  (secondaryRenderer ? secondaryRenderer->numRenderSteps() : 0);

    // Decide if we have any reason to flush pending work. We want to flush before updating the clip
    // state or making any permanent changes to a path atlas, since otherwise clip operations and/or
    // atlas entries for the current draw will be flushed.
    DstReadStrategy dstReadStrategy =
            dstReadRequired ? fDC->dstReadStrategy() : DstReadStrategy::kNoneRequired;
    const bool needsFlush = this->needsFlushBeforeDraw(numNewRenderSteps, dstReadStrategy);
    if (needsFlush) {
        if (pathAtlas != nullptr) {
            // We need to flush work for all devices associated with the current Recorder.
            // Otherwise we may end up with outstanding draws that depend on past atlas state.
            fRecorder->priv().flushTrackedDevices();
        } else {
            this->flushPendingWork(/*drawContext=*/nullptr);
        }
    }

    // If an atlas path renderer was chosen we need to insert the shape into the atlas and schedule
    // it to be drawn.
    std::optional<PathAtlas::MaskAndOrigin> atlasMask;  // only used if `pathAtlas != nullptr`
    if (pathAtlas != nullptr) {
        Rect clippedShapeBounds = clip.transformedShapeBounds().makeIntersect(clip.scissor());
        if (clippedShapeBounds.area() >= 0.8f * clip.transformedShapeBounds().area()) {
            // The clip isn't excluding very much of the original shape, so store the entire path
            // in the atlas to avoid redundant entries with slightly different clips.
            clippedShapeBounds = clip.transformedShapeBounds();
        }
        std::tie(renderer, atlasMask) = pathAtlas->addShape(clippedShapeBounds,
                                                            geometry.shape(),
                                                            localToDevice,
                                                            style);

        // If there was no space in the atlas and we haven't flushed already, then flush pending
        // work to clear up space in the atlas. If we had already flushed once (which would have
        // cleared the atlas) then the atlas is too small for this shape.
        if (!atlasMask && !needsFlush) {
            // We need to flush work for all devices associated with the current Recorder.
            // Otherwise we may end up with outstanding draws that depend on past atlas state.
            fRecorder->priv().flushTrackedDevices();

            // Try inserting the shape again.
            std::tie(renderer, atlasMask) = pathAtlas->addShape(clippedShapeBounds,
                                                                geometry.shape(),
                                                                localToDevice,
                                                                style);
        }

        if (!atlasMask) {
            SKGPU_LOG_E("Failed to add shape to atlas!");
            // TODO(b/285195175): This can happen if the atlas is not large enough or a compatible
            // atlas texture cannot be created. Handle the first case in `chooseRenderer` and make
            // sure that the atlas path renderer is not chosen if the path is larger than the atlas
            // texture.
            return;
        }
        // Since addShape() was successful we should have a valid Renderer now.
        SkASSERT(renderer && renderer->numRenderSteps() == 1 && !renderer->emitsPrimitiveColor());
    }

#if defined(SK_DEBUG)
    // Renderers and their component RenderSteps have flexibility in defining their
    // DepthStencilSettings. However, the clipping and ordering managed between Device and ClipStack
    // requires that only LESS or LEQUAL depth tests are used for draws recorded through the
    // client-facing, painters-order-oriented API. We assert here vs. in Renderer's constructor to
    // allow internal-oriented Renderers that are never selected for a "regular" draw call to have
    // more flexibility in their settings.
    SkASSERT(renderer);
    for (const RenderStep* step : renderer->steps()) {
        auto dss = step->depthStencilSettings();
        SkASSERT((!step->performsShading() || dss.fDepthTestEnabled) &&
                 (!dss.fDepthTestEnabled ||
                  dss.fDepthCompareOp == CompareOp::kLess ||
                  dss.fDepthCompareOp == CompareOp::kLEqual));
    }
#endif

    // Update the clip stack after issuing a flush (if it was needed). A draw will be recorded after
    // this point.
    DrawOrder order(fCurrentDepth.next());
    CompressedPaintersOrder clipOrder = fClip.updateClipStateForDraw(
            clip, clipElements, fColorDepthBoundsManager.get(), order.depth());

    // A draw's order always depends on the clips that must be drawn before it
    order.dependsOnPaintersOrder(clipOrder);
    // If a draw is not opaque, it must be drawn after the most recent draw it intersects with in
    // order to blend correctly.
    if (rendererCoverage != Coverage::kNone || dependsOnDst) {
        CompressedPaintersOrder prevDraw =
            fColorDepthBoundsManager->getMostRecentDraw(clip.drawBounds());
        order.dependsOnPaintersOrder(prevDraw);
    }

    // Now that the base paint order and draw bounds are finalized, if the Renderer relies on the
    // stencil attachment, we compute a secondary sorting field to allow disjoint draws to reorder
    // the RenderSteps across draws instead of in sequence for each draw.
    if (renderer->depthStencilFlags() & DepthStencilFlags::kStencil) {
        DisjointStencilIndex setIndex = fDisjointStencilSet->add(order.paintOrder(),
                                                                 clip.drawBounds());
        order.dependsOnStencil(setIndex);
    } else if (!dependsOnDst && renderer->coverage() == Coverage::kNone && style.isFillStyle() &&
               ((geometry.isEdgeAAQuad() && geometry.edgeAAQuad().isRect()) ||
                (geometry.isShape() && geometry.shape().isRect()))) {
        // Sort this draw front to back since it will not blend against what came before it.
        // We could do this for all opaque/non-blending draws but that can hurt the performance of
        // the std::sort in DrawPass::Make if it has to effectively reverse a large list. For now,
        // limit it to filled rectangles (here and for the later non-AA inner fill).
        order.reverseDepthAsStencil();
    }

    // TODO(b/330864257): This is an extra traversal of all paint effects, that can be avoided when
    // the paint key itself is determined inside this function.
    shading.notifyImagesInUse(fRecorder, fDC.get());

    // If an atlas path renderer was chosen, then record a single CoverageMaskShape draw.
    // The shape will be scheduled to be rendered or uploaded into the atlas during the
    // next invocation of flushPendingWork().
    if (pathAtlas != nullptr) {
        // Record the draw as a fill since stroking is handled by the atlas render/upload.
        SkASSERT(atlasMask.has_value());
        auto [mask, origin] = *atlasMask;
        fDC->recordDraw(renderer, Transform::Translate(origin.fX, origin.fY), Geometry(mask),
                        clip, order, &shading, nullptr, dependsOnDst, dstReadRequired);
    } else {
        if (styleType == SkStrokeRec::kStroke_Style ||
            styleType == SkStrokeRec::kHairline_Style ||
            styleType == SkStrokeRec::kStrokeAndFill_Style) {
            // For stroke-and-fill, 'renderer' is used for the fill and we always use the
            // TessellatedStrokes renderer; for stroke and hairline, 'renderer' is used.
            StrokeStyle stroke(style.getWidth(), style.getMiter(), style.getJoin(), style.getCap());
            fDC->recordDraw(styleType == SkStrokeRec::kStrokeAndFill_Style
                                   ? fRecorder->priv().rendererProvider()->tessellatedStrokes()
                                   : renderer,
                            localToDevice, geometry, clip, order, &shading, &stroke, dependsOnDst,
                            dstReadRequired);
        }
        if (styleType == SkStrokeRec::kFill_Style ||
            styleType == SkStrokeRec::kStrokeAndFill_Style) {
            // Possibly record an additional draw using the non-AA bounds renderer to fill the
            // interior with a renderer that can disable blending entirely.
            if (!innerFillBounds.isEmptyNegativeOrNaN()) {
                SkASSERT(!dependsOnDst && renderer->useNonAAInnerFill());
                DrawOrder orderWithoutCoverage{order.depth()};
                orderWithoutCoverage.dependsOnPaintersOrder(clipOrder);
                // The regular draw has analytic coverage, so isn't being sorted front to back, but
                // we do want to sort the inner fill to maximize overdraw reduction
                orderWithoutCoverage.reverseDepthAsStencil();

                fDC->recordDraw(fRecorder->priv().rendererProvider()->nonAABounds(), localToDevice,
                                Geometry(Shape(innerFillBounds)), clip, orderWithoutCoverage,
                                &shading, nullptr,  dependsOnDst, dstReadRequired);
                // Force the coverage draw to come after the non-AA draw in order to benefit from
                // early depth testing.
                order.dependsOnPaintersOrder(orderWithoutCoverage.paintOrder());
            }
            fDC->recordDraw(renderer, localToDevice, geometry, clip, order, &shading, nullptr,
                            dependsOnDst, dstReadRequired);
        }
    }

    // Post-draw book keeping (bounds manager, depth tracking, etc.)
    fColorDepthBoundsManager->recordDraw(clip.drawBounds(), order.paintOrder());
    fCurrentDepth = order.depth();

    // TODO(b/238758897): When we enable layer elision that depends on draws not overlapping, we
    // can use the `getMostRecentDraw()` query to determine that, although that will mean querying
    // even if the draw does not depend on dst (so should be only be used when the Device is an
    // elision candidate).
}

void Device::drawClipShape(const Transform& localToDevice,
                           const Shape& shape,
                           const Clip& clip,
                           DrawOrder order) {
    // A clip draw's state is almost fully defined by the ClipStack. The only thing we need
    // to account for is selecting a Renderer and tracking the stencil buffer usage.
    Geometry geometry{shape};
    auto [renderer, pathAtlas] = this->chooseRenderer(localToDevice,
                                                      geometry,
                                                      DefaultFillStyle(),
                                                      clip.transformedShapeBounds(),
                                                      /*requireMSAA=*/true);
    if (!renderer) {
        SKGPU_LOG_W("Skipping clip with no supported path renderer.");
        return;
    } else if (renderer->depthStencilFlags() & DepthStencilFlags::kStencil) {
        DisjointStencilIndex setIndex = fDisjointStencilSet->add(order.paintOrder(),
                                                                 clip.drawBounds());
        order.dependsOnStencil(setIndex);
    }

    // This call represents one of the deferred clip shapes that's already pessimistically counted
    // in needsFlushBeforeDraw(), so the DrawContext should have room to add it.
    SkASSERT(fDC->pendingRenderSteps() + renderer->numRenderSteps() < DrawList::kMaxRenderSteps);

    // Anti-aliased clipping requires the renderer to use MSAA to modify the depth per sample, so
    // analytic coverage renderers cannot be used.
    SkASSERT(renderer->coverage() == Coverage::kNone && renderer->requiresMSAA());
    SkASSERT(pathAtlas == nullptr);

    // Clips draws are depth-only (null PaintParams), and filled (null StrokeStyle).
    // TODO: Remove this CPU-transform once perspective is supported for all path renderers
    if (localToDevice.type() == Transform::Type::kPerspective) {
        SkPath devicePath = geometry.shape().asPath().makeTransform(localToDevice.matrix().asM33());
        fDC->recordDraw(renderer, Transform::Identity(), Geometry(Shape(devicePath)), clip, order,
                        /*paint*/nullptr, /*stroke*/nullptr, /*dependsOnDst*/false,
                        /*dstReadReq*/false);
    } else {
        fDC->recordDraw(renderer, localToDevice, geometry, clip, order, /*paint*/nullptr,
                        /*stroke*/nullptr, /*dependsOnDst*/false,  /*dstReadReq*/false);
    }
    // This ensures that draws recorded after this clip shape has been popped off the stack will
    // be unaffected by the Z value the clip shape wrote to the depth attachment.
    if (order.depth() > fCurrentDepth) {
        fCurrentDepth = order.depth();
    }
}

// TODO: Currently all Renderers are always defined, but with config options and caps that may not
// be the case, in which case chooseRenderer() will have to go through compatible choices.
std::pair<const Renderer*, PathAtlas*> Device::chooseRenderer(const Transform& localToDevice,
                                                              const Geometry& geometry,
                                                              const SkStrokeRec& style,
                                                              const Rect& drawBounds,
                                                              bool requireMSAA) const {
    const RendererProvider* renderers = fRecorder->priv().rendererProvider();
    SkASSERT(renderers);
    SkStrokeRec::Style type = style.getStyle();

    if (geometry.isSubRun()) {
        SkASSERT(!requireMSAA);
        sktext::gpu::RendererData rendererData = geometry.subRunData().rendererData();
        if (!rendererData.isSDF) {
            return {renderers->bitmapText(rendererData.isLCD, rendererData.maskFormat), nullptr};
        }
        // Even though the SkPaint can request subpixel rendering, we still need to match
        // this with the pixel geometry.
        bool useLCD = rendererData.isLCD &&
                      geometry.subRunData().pixelGeometry() != kUnknown_SkPixelGeometry;
        return {renderers->sdfText(useLCD), nullptr};
    } else if (geometry.isVertices()) {
        SkVerticesPriv info(geometry.vertices()->priv());
        return {renderers->vertices(info.mode(), info.hasColors(), info.hasTexCoords()), nullptr};
    } else if (geometry.isCoverageMaskShape()) {
        // drawCoverageMask() passes in CoverageMaskShapes that reference a provided texture.
        // The CoverageMask renderer can also be chosen later on if the shape is assigned to
        // to be rendered into the PathAtlas, in which case the 2nd return value is non-null.
        return {renderers->coverageMask(), nullptr};
    } else if (geometry.isEdgeAAQuad()) {
        SkASSERT(!requireMSAA && style.isFillStyle());
        // handled by specialized system, simplified from rects and round rects
        const EdgeAAQuad& quad = geometry.edgeAAQuad();
        if (quad.isRect() && (quad.edgeFlags() == EdgeAAQuad::Flags::kNone
#if !defined(SK_SKIP_PIXELALIGNED_QUAD_CHECK_GRAPHITE)
                              || is_pixel_aligned(quad.bounds(), localToDevice)
#endif
                             )) {
            // For non-AA rectangular quads, it can always use a coverage-less renderer; there's no
            // need to check for pixel alignment to avoid popping if MSAA is turned on because quad
            // tile edges will seam with each in either mode. We also switch to use the cover bounds
            // when the quad is pixel aligned to be consistent with drawRect Renderer handling.
            return {renderers->nonAABounds(), nullptr};
        } else {
            return {renderers->perEdgeAAQuad(), nullptr};
        }
    } else if (geometry.isAnalyticBlur()) {
        return {renderers->analyticBlur(), nullptr};
    } else if (!geometry.isShape()) {
        // We must account for new Geometry types with specific Renderers
        return {nullptr, nullptr};
    }

    const Shape& shape = geometry.shape();
    // We can't use this renderer if we require MSAA for an effect (i.e. clipping or stroke+fill).
    if (!requireMSAA && is_simple_shape(shape, type)) {
        // For pixel-aligned rects, use the the non-AA bounds renderer to avoid triggering any
        // dst-read requirement due to src blending.
        bool pixelAlignedRect = false;
        if (shape.isRect() && style.isFillStyle()) {
            pixelAlignedRect = is_pixel_aligned(shape.rect(), localToDevice);
        }

        if (shape.isEmpty() || pixelAlignedRect) {
            SkASSERT(!shape.isEmpty() || shape.inverted());
            return {renderers->nonAABounds(), nullptr};
        } else {
            return {renderers->analyticRRect(), nullptr};
        }
    }

    if (!requireMSAA && shape.isArc() &&
        SkScalarNearlyEqual(shape.arc().oval().width(), shape.arc().oval().height()) &&
        SkScalarAbs(shape.arc().sweepAngle()) < 360.f &&
        localToDevice.type() <= Transform::Type::kAffine) {
        float maxScale, minScale;
        std::tie(maxScale, minScale) = localToDevice.scaleFactors({0, 0});
        if (SkScalarNearlyEqual(maxScale, minScale)) {
            // Arc support depends on the style.
            SkStrokeRec::Style recStyle = style.getStyle();
            switch (recStyle) {
                case SkStrokeRec::kStrokeAndFill_Style:
                    // This produces a strange result that this op doesn't implement.
                    break;
                case SkStrokeRec::kFill_Style:
                    return {renderers->circularArc(), nullptr};
                case SkStrokeRec::kStroke_Style:
                case SkStrokeRec::kHairline_Style:
                    // Strokes that don't use the center point are supported with butt & round caps.
                    bool isWedge = shape.arc().isWedge();
                    bool isSquareCap = style.getCap() == SkPaint::kSquare_Cap;
                    if (!isWedge && !isSquareCap) {
                        return {renderers->circularArc(), nullptr};
                    }
                    break;
            }
        }
    }

    // Path rendering options. For now the strategy is very simple and not optimal:
    // I. Use tessellation if MSAA is required for an effect.
    // II: otherwise:
    //    1. Always use compute AA if supported unless it was excluded by ContextOptions or the
    //       compute renderer cannot render the shape efficiently yet (based on the result of
    //       `isSuitableForAtlasing`).
    //    2. Fall back to CPU raster AA if hardware MSAA is disabled or it was explicitly requested
    //       via ContextOptions (including if the path is small enough).
    //    3. Otherwise use tessellation.
#if defined(GPU_TEST_UTILS)
    PathRendererStrategy strategy = fRecorder->priv().caps()->requestedPathRendererStrategy();
#else
    PathRendererStrategy strategy = PathRendererStrategy::kDefault;
#endif

    PathAtlas* pathAtlas = nullptr;
    AtlasProvider* atlasProvider = fRecorder->priv().atlasProvider();

    // Prefer compute atlas draws if supported. This currently implicitly filters out clip draws as
    // they require MSAA. Eventually we may want to route clip shapes to the atlas as well but not
    // if hardware MSAA is required.
    if (atlasProvider->isAvailable(AtlasProvider::PathAtlasFlags::kCompute) &&
        use_compute_atlas_when_available(strategy)) {
        PathAtlas* atlas = fDC->getComputePathAtlas(fRecorder);
        SkASSERT(atlas);

        // Don't use the compute renderer if it can't handle the shape efficiently.
        if (atlas->isSuitableForAtlasing(drawBounds, fClip.conservativeBounds())) {
            pathAtlas = atlas;
        }
    }

    // Fall back to CPU rendered paths when multisampling is disabled and the compute atlas is not
    // available.
    const float minPathSizeForMSAA = fRecorder->priv().caps()->minPathSizeForMSAA();
    const bool useRasterAtlasByDefault =
            !fMSAASupported || all(drawBounds.size() <= minPathSizeForMSAA);
    if (!pathAtlas && atlasProvider->isAvailable(AtlasProvider::PathAtlasFlags::kRaster) &&
        (strategy == PathRendererStrategy::kRasterAA ||
         (strategy == PathRendererStrategy::kDefault && useRasterAtlasByDefault))) {
        // NOTE: RasterPathAtlas doesn't implement `PathAtlas::isSuitableForAtlasing` as it doesn't
        // reject paths (unlike ComputePathAtlas).
        pathAtlas = atlasProvider->getRasterPathAtlas();
    }

    if (!requireMSAA && pathAtlas) {
        // If we got here it means that we should draw with an atlas renderer if we can and avoid
        // resorting to one of the tessellating techniques.
        return {nullptr, pathAtlas};
    }

    // If we got here, it requires tessellated path rendering or an MSAA technique applied to a
    // simple shape (so we interpret them as paths to reduce the number of pipelines we need).

    // TODO: All shapes that select a tessellating path renderer need to be "pre-chopped" if they
    // are large enough to exceed the fixed count tessellation limits. Fills are pre-chopped to the
    // viewport bounds, strokes and stroke-and-fills are pre-chopped to the viewport bounds outset
    // by the stroke radius (hence taking the whole style and not just its type).

    if (type == SkStrokeRec::kStroke_Style ||
        type == SkStrokeRec::kHairline_Style) {
        // Unlike in Ganesh, the HW stroke tessellator can work with arbitrary paints since the
        // depth test prevents double-blending when there is transparency, thus we can HW stroke
        // any path regardless of its paint.
        // TODO: We treat inverse-filled strokes as regular strokes. We could handle them by
        // stenciling first with the HW stroke tessellator and then covering their bounds, but
        // inverse-filled strokes are not well-specified in our public canvas behavior so we may be
        // able to remove it.
        return {renderers->tessellatedStrokes(), nullptr};
    }

    // 'type' could be kStrokeAndFill, but in that case chooseRenderer() is meant to return the
    // fill renderer since tessellatedStrokes() will always be used for the stroke pass.
    if (shape.convex() && !shape.inverted()) {
        // TODO: Ganesh doesn't have a curve+middle-out triangles option for convex paths, but it
        // would be pretty trivial to spin up.
        return {renderers->convexTessellatedWedges(), nullptr};
    } else {
        const bool preferWedges =
                // TODO: Combine this heuristic with what is used in PathStencilCoverOp to choose
                // between wedges curves consistently in Graphite and Ganesh.
                (shape.isPath() && shape.path().countVerbs() < 50) ||
                drawBounds.area() <= (256 * 256);

        if (preferWedges) {
            return {renderers->stencilTessellatedWedges(shape.fillType()), nullptr};
        } else {
            return {renderers->stencilTessellatedCurvesAndTris(shape.fillType()), nullptr};
        }
    }
}

sk_sp<Task> Device::lastDrawTask() const {
    SkASSERT(this->isScratchDevice());
    return fLastTask;
}

void Device::flushPendingWork(DrawContext* drawContext) {
    TRACE_EVENT0("skia.gpu", TRACE_FUNC);

    // If this is a scratch device being flushed, it should only be flushing into the expected
    // next recording from when the Device was first created.
    SkASSERT(fRecorder);
    SkASSERT(fScopedRecordingID == 0 || fScopedRecordingID == fRecorder->priv().nextRecordingID());

    // TODO(b/330864257):  flushPendingWork() can be recursively called if this Device
    // recorded a picture shader draw and during a flush (triggered by snap or automatically from
    // reaching limits), the picture shader will be rendered to a new device. If that picture drawn
    // to the temporary device fills up an atlas it can trigger the global
    // recorder->flushTrackedDevices(), which will then encounter this device that is already in
    // the midst of flushing. To avoid crashing we only actually flush the first time this is called
    // and set a bit to early-out on any recursive calls.
    // This is not an ideal solution since the temporary Device's flush-the-world may have reset
    // atlas entries that the current Device's flushed draws will reference. But at this stage it's
    // not possible to split the already recorded draws into a before-list and an after-list that
    // can reference the old and new contents of the atlas. While avoiding the crash, this may cause
    // incorrect accesses to a shared atlas. Once paint data is extracted at draw time, picture
    // shaders will be resolved outside of flushes and then this will be fixed automatically.
    if (fIsFlushing) {
        return;
    } else {
        fIsFlushing = true;
    }

    this->internalFlush();
    sk_sp<Task> drawTask = fDC->snapDrawTask(fRecorder);
    if (drawContext) {
        drawContext->recordDependency(std::move(drawTask));
    } else {
        if (this->isScratchDevice()) {
            // TODO(b/323887221): Once shared atlas resources are less brittle, scratch devices
            // won't flush to the recorder at all and will only store the snapped task here.
            fLastTask = drawTask;
        } else {
            // Non-scratch devices do not need to point back to the last snapped task since they are
            // always added to the root task list.
            // TODO: It is currently possible for scratch devices to be flushed and instantiated
            // before their work is finished, meaning they will produce additional tasks to be
            // included in a follow-up Recording:
            // https://chat.google.com/room/AAAA2HlH94I/YU0XdFqX2Uw. However, in this case they no
            // longer appear scratch because the first Recording instantiated the targets. When
            // scratch devices are not actually registered with the Recorder and are only included
            // when they are drawn (e.g. restored), we should be able to assert that `fLastTask` is
            // null.
            fLastTask = nullptr;
        }

        if (drawTask) {
            fRecorder->priv().add(std::move(drawTask));
        }
    }

    fIsFlushing = false;
}

void Device::internalFlush() {
    TRACE_EVENT0("skia.gpu", TRACE_FUNC);
    ASSERT_SINGLE_OWNER

    // Push any pending uploads from the atlas provider that pending draws reference.
    fRecorder->priv().atlasProvider()->recordUploads(fDC.get());

    // Clip shapes are depth-only draws, but aren't recorded in the DrawContext until a flush in
    // order to determine the Z values for each element.
    fClip.recordDeferredClipDraws();

    // Flush all pending items to the internal task list and reset Device tracking state
    fDC->flush(fRecorder);

    fColorDepthBoundsManager->reset();
    fDisjointStencilSet->reset();
    fCurrentDepth = DrawOrder::kClearDepth;

     // Any cleanup in the AtlasProvider
    fRecorder->priv().atlasProvider()->compact();
}

bool Device::needsFlushBeforeDraw(int numNewRenderSteps, DstReadStrategy dstReadStrategy) {
    // Must also account for the elements in the clip stack that might need to be recorded.
    numNewRenderSteps += fClip.maxDeferredClipDraws() * Renderer::kMaxRenderSteps;
    bool needsFlush =
            // Need flush if we don't have room to record into the current list.
            (DrawList::kMaxRenderSteps - fDC->pendingRenderSteps()) < numNewRenderSteps ||
            // Need flush if this draw needs to copy the dst surface for reading.
            dstReadStrategy == DstReadStrategy::kTextureCopy;

    return needsFlush;
}

void Device::drawSpecial(SkSpecialImage* special,
                         const SkMatrix& localToDevice,
                         const SkSamplingOptions& sampling,
                         const SkPaint& paint,
                         SkCanvas::SrcRectConstraint constraint) {
    SkASSERT(!paint.getMaskFilter() && !paint.getImageFilter());

    sk_sp<SkImage> img = special->asImage();
    if (!img || !as_IB(img)->isGraphiteBacked()) {
        SKGPU_LOG_W("Couldn't get Graphite-backed special image as image");
        return;
    }

    SkPaint paintWithShader(paint);
    SkRect dst = SkModifyPaintAndDstForDrawImageRect(
            img.get(),
            sampling,
            /*src=*/SkRect::Make(special->subset()),
            /*dst=*/SkRect::MakeIWH(special->width(), special->height()),
            /*strictSrcSubset=*/constraint == SkCanvas::kStrict_SrcRectConstraint,
            &paintWithShader);
    if (dst.isEmpty()) {
        return;
    }

    // The image filtering and layer code paths often rely on the paint being non-AA to avoid
    // coverage operations. To stay consistent with the other backends, we use an edge AA "quad"
    // whose flags match the paint's AA request.
    EdgeAAQuad::Flags aaFlags = paint.isAntiAlias() ? EdgeAAQuad::Flags::kAll
                                                    : EdgeAAQuad::Flags::kNone;
    this->drawGeometry(Transform(SkM44(localToDevice)),
                       Geometry(EdgeAAQuad(dst, aaFlags)),
                       paintWithShader,
                       DefaultFillStyle(),
                       DrawFlags::kIgnorePathEffect);
}

void Device::drawCoverageMask(const SkSpecialImage* mask,
                              const SkMatrix& localToDevice,
                              const SkSamplingOptions& sampling,
                              const SkPaint& paint) {
    CoverageMaskShape::MaskInfo maskInfo{/*fTextureOrigin=*/{SkTo<uint16_t>(mask->subset().fLeft),
                                                             SkTo<uint16_t>(mask->subset().fTop)},
                                         /*fMaskSize=*/{SkTo<uint16_t>(mask->width()),
                                                        SkTo<uint16_t>(mask->height())}};

    auto maskProxyView = AsView(mask->asImage());
    if (!maskProxyView) {
        SKGPU_LOG_W("Couldn't get Graphite-backed special image as texture proxy view");
        return;
    }

    // Every other "Image" draw reaches the underlying texture via AddToKey/NotifyInUse, which
    // handles notifying the image and either flushing the linked surface or attaching draw tasks
    // from a scratch device to the current draw context. In this case, 'mask' is very likely to
    // be linked to a scratch device, but we must perform the same notifyInUse manually here because
    // the texture is consumed by the RenderStep and not part of the PaintParams.
    static_cast<Image_Base*>(mask->asImage().get())->notifyInUse(fRecorder, fDC.get());

    // CoverageMaskShape() wraps a Shape when it's used as a PathAtlas, but in this case the
    // original shape has been long lost, so just use a Rect that bounds the image.
    CoverageMaskShape maskShape{Shape{Rect::WH((float)mask->width(), (float)mask->height())},
                                // We store a ref to the textureProxy to keep it alive.
                                maskProxyView.refProxy(),
                                // Use the active local-to-device transform for this since it
                                // determines the local coords for evaluating the skpaint, whereas
                                // the provided 'localToDevice' just places the coverage mask.
                                this->localToDeviceTransform().inverse(),
                                maskInfo};

    this->drawGeometry(Transform(SkM44(localToDevice)),
                       Geometry(maskShape),
                       paint,
                       DefaultFillStyle(),
                       DrawFlags::kIgnorePathEffect);
}

sk_sp<SkSpecialImage> Device::snapSpecial(const SkIRect& subset, bool forceCopy) {
    // NOTE: snapSpecial() can be called even after the device has been marked immutable (null
    // recorder), but in those cases it should not be a copy and just returns the image view.
    sk_sp<Image> deviceImage;
    SkIRect finalSubset;
    if (forceCopy || !this->readSurfaceView() || this->readSurfaceView().proxy()->isFullyLazy()) {
        deviceImage = this->makeImageCopy(
                subset, Budgeted::kYes, Mipmapped::kNo, SkBackingFit::kApprox);
        finalSubset = SkIRect::MakeSize(subset.size());
    } else {
        // TODO(b/323886870): For now snapSpecial() force adds the pending work to the recorder's
        // root task list. Once shared atlas management is solved and DrawTasks can be nested in a
        // graph then this can go away in favor of auto-flushing through the image's linked device.
        if (fRecorder) {
            this->flushPendingWork(/*drawContext=*/nullptr);
        }
        deviceImage = Image::WrapDevice(sk_ref_sp(this));
        finalSubset = subset;
    }

    if (!deviceImage) {
        return nullptr;
    }

    // For non-copying "snapSpecial", the semantics are returning an image view of the surface data,
    // and relying on higher-level draw and restore logic for the contents to make sense.
    return SkSpecialImages::MakeGraphite(
            fRecorder, finalSubset, std::move(deviceImage), this->surfaceProps());
}

sk_sp<skif::Backend> Device::createImageFilteringBackend(const SkSurfaceProps& surfaceProps,
                                                         SkColorType colorType) const {
    return skif::MakeGraphiteBackend(fRecorder, surfaceProps, colorType);
}

TextureProxy* Device::target() { return fDC->target(); }

TextureProxyView Device::readSurfaceView() const { return fDC->readSurfaceView(); }

bool Device::isScratchDevice() const {
    // Scratch device status is inferred from whether or not the Device's target is instantiated.
    // By default devices start out un-instantiated unless they are wrapping an existing backend
    // texture (definitely not a scratch scenario), or Surface explicitly instantiates the target
    // before returning to the client (not a scratch scenario).
    //
    // Scratch device targets are instantiated during the prepareResources() phase of
    // Recorder::snap(). Truly scratch devices that have gone out of scope as intended will have
    // already been destroyed at this point. Scratch devices that become longer-lived (linked to
    // a client-owned object) automatically transition to non-scratch usage.
    return !fDC->target()->isInstantiated() && !fDC->target()->isLazy();
}

sk_sp<sktext::gpu::Slug> Device::convertGlyphRunListToSlug(const sktext::GlyphRunList& glyphRunList,
                                                           const SkPaint& paint) {
    return sktext::gpu::SlugImpl::Make(this->localToDevice(),
                                       glyphRunList,
                                       paint,
                                       this->strikeDeviceInfo(),
                                       SkStrikeCache::GlobalStrikeCache());
}

void Device::drawSlug(SkCanvas* canvas, const sktext::gpu::Slug* slug, const SkPaint& paint) {
    auto slugImpl = static_cast<const sktext::gpu::SlugImpl*>(slug);
    slugImpl->subRuns()->draw(canvas, slugImpl->origin(), paint, slugImpl, this->atlasDelegate());
}

bool Device::drawBlurredRRect(const SkRRect& rrect, const SkPaint& paint, float deviceSigma) {
    SkStrokeRec style(paint);
    if (skgpu::BlurIsEffectivelyIdentity(deviceSigma)) {
        this->drawGeometry(this->localToDeviceTransform(),
                           Geometry(rrect.isRect() ? Shape(rrect.rect()) : Shape(rrect)),
                           paint,
                           style);
        return true;
    }

    std::optional<AnalyticBlurMask> analyticBlur = AnalyticBlurMask::Make(
            this->recorder(), this->localToDeviceTransform(), deviceSigma, rrect);
    if (!analyticBlur) {
        return false;
    }

    this->drawGeometry(this->localToDeviceTransform(), Geometry(*analyticBlur), paint, style);
    return true;
}

} // namespace skgpu::graphite
