[graphite] Simplify copies and special-images around Image

Graphite's SpecialImage now wraps an SkImage directly, which
simplifies its creation logic and now allows YUVA backed images to
be wrapped as special images (which could arise if an SkImage was
wrapped in an image filter and that image were YUVA).

Copy logic had been spread out between Device, Image, and
TextureProxyView to support copies needed for the old special
image definition. This moves the copy-as-draw fallback to Image,
although the blit path has to be replicated between Device (for
non texturable surfaces) and Image. Follow-up CLs will add a
Image::MakeCopy() CL that fully consolidates copy-by-blit and
copy-as-draw and further consolidates a lot of the repeated
copy-as-draw logic in other Image and Image_YUVA API functions into
Image_Base.

Updated the copy-as-draw logic to use a scratch surface instead of
SkSurfaces::RenderTarget.

Bug: b/323887207
Change-Id: I4b75c6b1b735a67e44a2617180bf3eb153ab8595
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/833457
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: James Godfrey-Kittle <jamesgk@google.com>
diff --git a/src/gpu/graphite/Device.cpp b/src/gpu/graphite/Device.cpp
index 7c90623..3288984 100644
--- a/src/gpu/graphite/Device.cpp
+++ b/src/gpu/graphite/Device.cpp
@@ -403,53 +403,47 @@
     return SkSurfaces::RenderTarget(fRecorder, ii, Mipmapped::kNo, &props);
 }
 
-TextureProxyView Device::createCopy(const SkIRect* subset,
-                                    Mipmapped mipmapped,
-                                    SkBackingFit backingFit) {
-    const TextureProxyView& srcView = this->readSurfaceView();
-    if (!srcView) {
-        return {};
-    }
-
-    if (!fRecorder->priv().caps()->supportsReadPixels(srcView.proxy()->textureInfo())) {
-        if (!fRecorder->priv().caps()->isTexturable(srcView.proxy()->textureInfo())) {
-            return {};
-        }
-        // We ignore backingFit here and always make a tight texture.
-        auto size = subset ? subset->size() : this->size();
-        auto surface = SkSurfaces::RenderTarget(fRecorder,
-                                                this->imageInfo().makeDimensions(size),
-                                                mipmapped);
-
-        // Any pending work is flushed automatically when this image is drawn to `surface`
-        auto image = Image::MakeView(sk_ref_sp(this));
-        SkPaint paint;
-        paint.setBlendMode(SkBlendMode::kSrc);
-        auto pt = subset ? subset->topLeft() : SkIPoint{0, 0};
-        surface->getCanvas()->drawImage(image, -pt.x(), -pt.y(), SkFilterMode::kNearest, &paint);
-        Flush(surface.get());
-        const TextureProxyView& readView = static_cast<Surface*>(surface.get())->readSurfaceView();
-        // TODO(b/297344089): For mipmapped surfaces, the above Flush() also generates the mipmaps.
-        // When automatic mipmap generation happens lazily on first-read, this function should
-        // explicitly trigger the one-time mipmap generation.
-        SkASSERT(readView.proxy()->mipmapped() == mipmapped);
-        return readView;
-    }
-
-    SkIRect srcRect = subset ? *subset : SkIRect::MakeSize(this->imageInfo().dimensions());
+sk_sp<Image> Device::makeImageCopy(const SkIRect& subset,
+                                   Budgeted budgeted,
+                                   Mipmapped mipmapped,
+                                   SkBackingFit backingFit) {
+    SkASSERT(fRecorder);
     this->flushPendingWorkToRecorder();
-    return TextureProxyView::Copy(this->recorder(),
-                                  this->imageInfo().colorInfo(),
-                                  srcView,
-                                  srcRect,
-                                  mipmapped,
-                                  backingFit);
+
+    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};
+    }
+    TextureProxyView copiedView = TextureProxyView::Copy(fRecorder,
+                                                         colorInfo,
+                                                         srcView,
+                                                         subset,
+                                                         budgeted,
+                                                         mipmapped,
+                                                         backingFit);
+    if (!copiedView) {
+        // The surface didn't support read pixels, so copy-as-draw using the image view
+        sk_sp<Image> sampleableSurface = Image::MakeView(sk_ref_sp(this));
+        if (sampleableSurface) {
+            return sampleableSurface->copyImage(fRecorder, subset, budgeted, mipmapped, backingFit);
+        }
+        // Cannot be copied by blit nor by a draw, so cannot be done.
+        return nullptr;
+    }
+
+    return sk_make_sp<Image>(kNeedNewImageUniqueID, std::move(copiedView), colorInfo);
 }
 
 TextureProxyView TextureProxyView::Copy(Recorder* recorder,
                                         const SkColorInfo& srcColorInfo,
                                         const TextureProxyView& srcView,
                                         SkIRect srcRect,
+                                        Budgeted budgeted,
                                         Mipmapped mipmapped,
                                         SkBackingFit backingFit) {
     SkASSERT(!(mipmapped == Mipmapped::kYes && backingFit == SkBackingFit::kApprox));
@@ -457,14 +451,20 @@
     SkASSERT(srcView.proxy()->isFullyLazy() ||
              SkIRect::MakeSize(srcView.proxy()->dimensions()).contains(srcRect));
 
+    if (!recorder->priv().caps()->supportsReadPixels(srcView.proxy()->textureInfo())) {
+        // Caller is responsible for copy-as-draw fallbacks
+        return {};
+    }
+
     skgpu::graphite::TextureInfo textureInfo =
             recorder->priv().caps()->getTextureInfoForSampledCopy(srcView.proxy()->textureInfo(),
                                                                   mipmapped);
+
     sk_sp<TextureProxy> dest = TextureProxy::Make(
             recorder->priv().caps(),
             backingFit == SkBackingFit::kApprox ? GetApproxSize(srcRect.size()) : srcRect.size(),
             textureInfo,
-            skgpu::Budgeted::kNo);
+            budgeted);
     if (!dest) {
         return {};
     }
@@ -1569,7 +1569,7 @@
                                          /*fMaskSize=*/{SkTo<uint16_t>(mask->width()),
                                                         SkTo<uint16_t>(mask->height())}};
 
-    auto maskProxyView = SkSpecialImages::AsTextureProxyView(mask);
+    auto maskProxyView = AsView(mask->asImage());
     if (!maskProxyView) {
         SKGPU_LOG_W("Couldn't get Graphite-backed special image as texture proxy view");
         return;
@@ -1615,37 +1615,33 @@
 }
 
 sk_sp<SkSpecialImage> Device::snapSpecial(const SkIRect& subset, bool forceCopy) {
-    if (fRecorder) {
-        this->flushPendingWorkToRecorder();
+    // 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->flushPendingWorkToRecorder();
+        }
+        deviceImage = Image::MakeView(sk_ref_sp(this));
+        finalSubset = subset;
     }
 
-    SkIRect finalSubset = subset;
-    TextureProxyView view = this->readSurfaceView();
-    if (forceCopy || !view || view.proxy()->isFullyLazy()) {
-        // snapSpecial() can be called after setImmutable() is called, but in that case it should
-        // never be a copy (that would otherwise require access to the recorder), and shouldn't be
-        // a non-readable or fully lazy proxy (since those come from client Surfaces).
-        SkASSERT(fRecorder);
-        if (!fRecorder) {
-            return nullptr;
-        }
-
-        // TODO: this doesn't address the non-readable surface view case, in which view is empty and
-        // createCopy will return an empty view as well.
-        view = this->createCopy(&subset, Mipmapped::kNo, SkBackingFit::kApprox);
-        if (!view) {
-            return nullptr;
-        }
-        finalSubset = SkIRect::MakeSize(subset.size());
+    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(finalSubset,
-                                         kNeedNewImageUniqueID_SpecialImage,
-                                         std::move(view),
-                                         this->imageInfo().colorInfo(),
-                                         this->surfaceProps());
+    return SkSpecialImages::MakeGraphite(
+            fRecorder, finalSubset, std::move(deviceImage), this->surfaceProps());
 }
 
 sk_sp<skif::Backend> Device::createImageFilteringBackend(const SkSurfaceProps& surfaceProps,
diff --git a/src/gpu/graphite/Device.h b/src/gpu/graphite/Device.h
index 738806c..81a6b6f 100644
--- a/src/gpu/graphite/Device.h
+++ b/src/gpu/graphite/Device.h
@@ -31,6 +31,7 @@
 class DrawContext;
 enum class DstReadRequirement;
 class Geometry;
+class Image;
 class PaintParams;
 class Recorder;
 class Renderer;
@@ -74,8 +75,6 @@
     // from the DrawContext as a RenderPassTask and records it in the Device's recorder.
     void flushPendingWorkToRecorder();
 
-    TextureProxyView createCopy(const SkIRect* subset, Mipmapped, SkBackingFit);
-
     const Transform& localToDeviceTransform();
 
     // Flushes any pending work to the recorder and then deregisters and abandons the recorder.
@@ -84,7 +83,10 @@
     SkStrikeDeviceInfo strikeDeviceInfo() const override;
 
     TextureProxy* target();
+    // May be null if target is not sampleable.
     TextureProxyView readSurfaceView() const;
+    // Can succeed if target is readable but not sampleable. Assumes 'subset' is contained in bounds
+    sk_sp<Image> makeImageCopy(const SkIRect& subset, Budgeted, Mipmapped, SkBackingFit);
 
     // SkCanvas only uses drawCoverageMask w/o this staging flag, so only enable
     // mask filters in clients that have finished migrating.
diff --git a/src/gpu/graphite/Image_Graphite.cpp b/src/gpu/graphite/Image_Graphite.cpp
index c69a8d6..65678f3 100644
--- a/src/gpu/graphite/Image_Graphite.cpp
+++ b/src/gpu/graphite/Image_Graphite.cpp
@@ -22,6 +22,7 @@
 #include "src/gpu/graphite/Log.h"
 #include "src/gpu/graphite/RecorderPriv.h"
 #include "src/gpu/graphite/ResourceProvider.h"
+#include "src/gpu/graphite/Surface_Graphite.h"
 #include "src/gpu/graphite/Texture.h"
 
 #if defined(GRAPHITE_TEST_UTILS)
@@ -44,6 +45,9 @@
     if (!proxy) {
         return nullptr;
     }
+    // NOTE: If the device was created with an approx backing fit, its SkImageInfo reports the
+    // logical dimensions, but its proxy has the approximate fit. These larger dimensions are
+    // propagated to the SkImageInfo of this image view.
     sk_sp<Image> view = sk_make_sp<Image>(kNeedNewImageUniqueID,
                                           std::move(proxy),
                                           device->imageInfo().colorInfo());
@@ -70,43 +74,77 @@
 
     // optimization : return self if the subset == our bounds and requirements met
     if (bounds == subset && (!requiredProps.fMipmapped || this->hasMipmaps())) {
-        const SkImage* image = this;
-        return sk_ref_sp(const_cast<SkImage*>(image));
+        return sk_ref_sp(this);
     }
 
-    return this->copyImage(subset, recorder, requiredProps);
+    // The copied image is not considered budgeted because this is a client-invoked API and they
+    // will own the image.
+    return this->copyImage(recorder,
+                           subset,
+                           Budgeted::kNo,
+                           requiredProps.fMipmapped ? Mipmapped::kYes : Mipmapped::kNo,
+                           SkBackingFit::kExact);
 }
 
 sk_sp<SkImage> Image::makeTextureImage(Recorder* recorder, RequiredProperties requiredProps) const {
     if (!requiredProps.fMipmapped || this->hasMipmaps()) {
-        const SkImage* image = this;
-        return sk_ref_sp(const_cast<SkImage*>(image));
+        return sk_ref_sp(this);
     }
 
+    // The copied image is not considered budgeted because this is a client-invoked API and they
+    // will own the image.
     const SkIRect bounds = SkIRect::MakeWH(this->width(), this->height());
-    return this->copyImage(bounds, recorder, requiredProps);
+    return this->copyImage(recorder,
+                           bounds,
+                           Budgeted::kNo,
+                           requiredProps.fMipmapped ? Mipmapped::kYes : Mipmapped::kNo,
+                           SkBackingFit::kExact);
 }
 
-sk_sp<SkImage> Image::copyImage(const SkIRect& subset,
-                                Recorder* recorder,
-                                RequiredProperties requiredProps) const {
+sk_sp<Image> Image::copyImage(Recorder* recorder,
+                              const SkIRect& subset,
+                              Budgeted budgeted,
+                              Mipmapped mipmapped,
+                              SkBackingFit backingFit) const {
+    SkASSERT(!(mipmapped == Mipmapped::kYes && backingFit == SkBackingFit::kApprox));
     const TextureProxyView& srcView = this->textureProxyView();
     if (!srcView) {
         return nullptr;
     }
 
+    // Attempt copying as a blit first.
+    const SkColorInfo& colorInfo = this->imageInfo().colorInfo();
     this->notifyInUse(recorder);
-
-    auto mm = requiredProps.fMipmapped ? skgpu::Mipmapped::kYes : skgpu::Mipmapped::kNo;
-    TextureProxyView copiedView = TextureProxyView::Copy(
-            recorder, this->imageInfo().colorInfo(), srcView, subset, mm, SkBackingFit::kExact);
-    if (!copiedView) {
-        return nullptr;
+    TextureProxyView copiedView = TextureProxyView::Copy(recorder,
+                                                         colorInfo,
+                                                         srcView,
+                                                         subset,
+                                                         budgeted,
+                                                         mipmapped,
+                                                         backingFit);
+    if (copiedView) {
+        return sk_make_sp<Image>(kNeedNewImageUniqueID, std::move(copiedView), colorInfo);
     }
 
-    return sk_sp<Image>(new Image(kNeedNewImageUniqueID,
-                                  std::move(copiedView),
-                                  this->imageInfo().colorInfo()));
+    // Perform the copy as a draw; since the proxy has been wrapped in an Image, it should be
+    // texturable.
+    SkASSERT(recorder->priv().caps()->isTexturable(srcView.proxy()->textureInfo()));
+
+    // The surface goes out of scope when we return, so it can be scratch, but it may or may
+    // not be budgeted depending on how the copied image is used (or returned to the client).
+    // TODO: Move copy-as-draw to the default Image_Base implementation since that handles the
+    // YUVA case entirely.
+    auto surface = Surface::MakeScratch(recorder,
+                                        this->imageInfo().makeDimensions(subset.size()),
+                                        budgeted,
+                                        mipmapped,
+                                        backingFit);
+    SkPaint paint;
+    paint.setBlendMode(SkBlendMode::kSrc);
+    surface->getCanvas()->drawImage(this, -subset.left(), -subset.top(),
+                                    SkFilterMode::kNearest, &paint);
+    // And the image draw into `surface` is flushed when it goes out of scope
+    return surface->asImage();
 }
 
 sk_sp<SkImage> Image::onReinterpretColorSpace(sk_sp<SkColorSpace> newCS) const {
diff --git a/src/gpu/graphite/Image_Graphite.h b/src/gpu/graphite/Image_Graphite.h
index 60b3210..8b8ce77 100644
--- a/src/gpu/graphite/Image_Graphite.h
+++ b/src/gpu/graphite/Image_Graphite.h
@@ -34,10 +34,14 @@
 
     const TextureProxyView& textureProxyView() const { return fTextureProxyView; }
 
+    // Always copy this image, even if 'subset' and mipmapping match this image exactly.
+    sk_sp<Image> copyImage(Recorder*, const SkIRect& subset,
+                           Budgeted, Mipmapped, SkBackingFit) const;
+
     SkImage_Base::Type type() const override { return SkImage_Base::Type::kGraphite; }
 
     bool onHasMipmaps() const override {
-        return fTextureProxyView.proxy()->mipmapped() == skgpu::Mipmapped::kYes;
+        return fTextureProxyView.proxy()->mipmapped() == Mipmapped::kYes;
     }
 
     bool onIsProtected() const override {
@@ -67,15 +71,16 @@
 #endif
 
 private:
-    sk_sp<SkImage> copyImage(const SkIRect& subset, Recorder*, RequiredProperties) const;
-    using Image_Base::onMakeSubset;
     sk_sp<SkImage> onMakeSubset(Recorder*, const SkIRect&, RequiredProperties) const override;
-    using Image_Base::onMakeColorTypeAndColorSpace;
     sk_sp<SkImage> makeColorTypeAndColorSpace(Recorder*,
                                               SkColorType targetCT,
                                               sk_sp<SkColorSpace> targetCS,
                                               RequiredProperties) const override;
 
+    // Include the no-op Ganesh functions to avoid warnings about hidden virtuals.
+    using Image_Base::onMakeSubset;
+    using Image_Base::onMakeColorTypeAndColorSpace;
+
     TextureProxyView fTextureProxyView;
 };
 
diff --git a/src/gpu/graphite/SpecialImage_Graphite.cpp b/src/gpu/graphite/SpecialImage_Graphite.cpp
index eebfa77..9bc5923 100644
--- a/src/gpu/graphite/SpecialImage_Graphite.cpp
+++ b/src/gpu/graphite/SpecialImage_Graphite.cpp
@@ -17,44 +17,36 @@
 
 namespace skgpu::graphite {
 
-class SkSpecialImage_Graphite final : public SkSpecialImage {
+class SpecialImage final : public SkSpecialImage {
 public:
-    SkSpecialImage_Graphite(const SkIRect& subset,
-                            uint32_t uniqueID,
-                            TextureProxyView view,
-                            const SkColorInfo& colorInfo,
-                            const SkSurfaceProps& props)
-            : SkSpecialImage(subset, uniqueID, colorInfo, props)
-            , fTextureProxyView(std::move(view)) {
+    SpecialImage(const SkIRect& subset, sk_sp<SkImage> image, const SkSurfaceProps& props)
+            : SkSpecialImage(subset, image->uniqueID(), image->imageInfo().colorInfo(), props)
+            , fImage(std::move(image)) {
+        SkASSERT(as_IB(fImage)->isGraphiteBacked());
     }
 
     size_t getSize() const override {
-        // TODO: return VRAM size here
-        return 0;
+        return fImage->textureSize();
     }
 
     bool isGraphiteBacked() const override { return true; }
 
-    TextureProxyView textureProxyView() const { return fTextureProxyView; }
-
     SkISize backingStoreDimensions() const override {
-        return fTextureProxyView.proxy()->dimensions();
+        return fImage->dimensions();
     }
 
     sk_sp<SkSpecialImage> onMakeBackingStoreSubset(const SkIRect& subset) const override {
-        return SkSpecialImages::MakeGraphite(subset,
-                                             this->uniqueID(),
-                                             fTextureProxyView,
-                                             this->colorInfo(),
-                                             this->props());
+        SkASSERT(fImage->bounds().contains(subset));
+        return sk_make_sp<skgpu::graphite::SpecialImage>(subset, fImage, this->props());
     }
 
-    sk_sp<SkImage> asImage() const override {
-        return sk_make_sp<Image>(this->uniqueID(), fTextureProxyView, this->colorInfo());
-    }
+    sk_sp<SkImage> asImage() const override { return fImage; }
 
 private:
-    TextureProxyView fTextureProxyView;
+    // TODO(b/299474380): SkSpecialImage is intended to go away in favor of just using SkImages
+    // and tracking the intended srcRect explicitly in skif::FilterResult. Since Graphite tracks
+    // device-linked textures via Images, the graphite special image just wraps an image.
+    sk_sp<SkImage> fImage;
 };
 
 } // namespace skgpu::graphite
@@ -65,15 +57,21 @@
                                    const SkIRect& subset,
                                    sk_sp<SkImage> image,
                                    const SkSurfaceProps& props) {
-    if (!recorder || !image || subset.isEmpty()) {
+    // 'recorder' can be null if we're wrapping a graphite-backed image since there's no work that
+    // needs to be added. This can happen when snapping a special image from a Device that's been
+    // marked as immutable and abandoned its recorder.
+    if (!image || subset.isEmpty()) {
         return nullptr;
     }
 
     SkASSERT(image->bounds().contains(subset));
 
-    // This will work even if the image is a raster-backed image and the Recorder's
-    // client ImageProvider does a valid upload.
+    // Use the Recorder's client ImageProvider to convert to a graphite-backed image when
+    // possible, but this does not necessarily mean the provider will produce a valid image.
     if (!as_IB(image)->isGraphiteBacked()) {
+        if (!recorder) {
+            return nullptr;
+        }
         auto [graphiteImage, _] =
                 skgpu::graphite::GetGraphiteBacked(recorder, image.get(), {});
         if (!graphiteImage) {
@@ -83,37 +81,7 @@
         image = graphiteImage;
     }
 
-    // TODO(b/323887207): Graphite's SkSpecialImage class should just wrap Image to avoid losing
-    // any linked Device. When that happens, linked devices will automatically be flushed even when
-    // it's the special image being drawn, not the originating `image`. For now, notify the original
-    static_cast<skgpu::graphite::Image_Base*>(image.get())->notifyInUse(recorder);
-    return MakeGraphite(subset,
-                        image->uniqueID(),
-                        skgpu::graphite::AsView(image.get()),
-                        image->imageInfo().colorInfo(),
-                        props);
-}
-
-sk_sp<SkSpecialImage> MakeGraphite(const SkIRect& subset,
-                                   uint32_t uniqueID,
-                                   skgpu::graphite::TextureProxyView view,
-                                   const SkColorInfo& colorInfo,
-                                   const SkSurfaceProps& props) {
-    if (!view) {
-        return nullptr;
-    }
-
-    SkASSERT(SkIRect::MakeSize(view.dimensions()).contains(subset));
-    return sk_make_sp<skgpu::graphite::SkSpecialImage_Graphite>(subset, uniqueID,
-                                                                std::move(view), colorInfo, props);
-}
-
-skgpu::graphite::TextureProxyView AsTextureProxyView(const SkSpecialImage* img) {
-    if (!img || !img->isGraphiteBacked()) {
-        return {};
-    }
-    auto grImg = static_cast<const skgpu::graphite::SkSpecialImage_Graphite*>(img);
-    return grImg->textureProxyView();
+    return sk_make_sp<skgpu::graphite::SpecialImage>(subset, std::move(image), props);
 }
 
 }  // namespace SkSpecialImages
diff --git a/src/gpu/graphite/SpecialImage_Graphite.h b/src/gpu/graphite/SpecialImage_Graphite.h
index bb4b196..7ad7dfd 100644
--- a/src/gpu/graphite/SpecialImage_Graphite.h
+++ b/src/gpu/graphite/SpecialImage_Graphite.h
@@ -32,17 +32,6 @@
                                    sk_sp<SkImage>,
                                    const SkSurfaceProps&);
 
-sk_sp<SkSpecialImage> MakeGraphite(const SkIRect& subset,
-                                   uint32_t uniqueID,
-                                   skgpu::graphite::TextureProxyView,
-                                   const SkColorInfo&,
-                                   const SkSurfaceProps&);
-
-skgpu::graphite::TextureProxyView AsTextureProxyView(const SkSpecialImage*);
-inline skgpu::graphite::TextureProxyView AsTextureProxyView(sk_sp<const SkSpecialImage> img) {
-    return AsTextureProxyView(img.get());
-}
-
 }  // namespace SkSpecialImages
 
 #endif
diff --git a/src/gpu/graphite/Surface_Graphite.cpp b/src/gpu/graphite/Surface_Graphite.cpp
index 3a8f61d..bd08588 100644
--- a/src/gpu/graphite/Surface_Graphite.cpp
+++ b/src/gpu/graphite/Surface_Graphite.cpp
@@ -62,7 +62,7 @@
     return this->makeImageCopy(subset, srcView.mipmapped());
 }
 
-sk_sp<SkImage> Surface::asImage() const {
+sk_sp<Image> Surface::asImage() const {
     if (this->hasCachedImage()) {
         SKGPU_LOG_W("Intermingling makeImageSnapshot and asImage calls may produce "
                     "unexpected results. Please use either the old _or_ new API.");
@@ -70,19 +70,15 @@
     return fImageView;
 }
 
-sk_sp<SkImage> Surface::makeImageCopy(const SkIRect* subset, Mipmapped mipmapped) const {
+sk_sp<Image> Surface::makeImageCopy(const SkIRect* subset, Mipmapped mipmapped) const {
     if (this->hasCachedImage()) {
         SKGPU_LOG_W("Intermingling makeImageSnapshot and asImage calls may produce "
                     "unexpected results. Please use either the old _or_ new API.");
     }
-    TextureProxyView srcView = fDevice->createCopy(subset, mipmapped, SkBackingFit::kExact);
-    if (!srcView) {
-        return nullptr;
-    }
 
-    return sk_sp<Image>(new Image(kNeedNewImageUniqueID,
-                                  std::move(srcView),
-                                  this->imageInfo().colorInfo()));
+    SkIRect srcRect = subset ? *subset : SkIRect::MakeSize(this->imageInfo().dimensions());
+    // NOTE: Must copy through fDevice and not fImageView if the surface's texture is not sampleable
+    return fDevice->makeImageCopy(srcRect, Budgeted::kNo, mipmapped, SkBackingFit::kExact);
 }
 
 void Surface::onWritePixels(const SkPixmap& pixmap, int x, int y) {
diff --git a/src/gpu/graphite/Surface_Graphite.h b/src/gpu/graphite/Surface_Graphite.h
index c7b588e..2c1732f 100644
--- a/src/gpu/graphite/Surface_Graphite.h
+++ b/src/gpu/graphite/Surface_Graphite.h
@@ -80,8 +80,8 @@
     sk_sp<const SkCapabilities> onCapabilities() override;
 
     TextureProxyView readSurfaceView() const;
-    sk_sp<SkImage> asImage() const;
-    sk_sp<SkImage> makeImageCopy(const SkIRect* subset, Mipmapped) const;
+    sk_sp<Image> asImage() const;
+    sk_sp<Image> makeImageCopy(const SkIRect* subset, Mipmapped) const;
     TextureProxy* backingTextureProxy() const;
 
 private:
diff --git a/src/gpu/graphite/TextureProxyView.h b/src/gpu/graphite/TextureProxyView.h
index c4de615..96f28a3 100644
--- a/src/gpu/graphite/TextureProxyView.h
+++ b/src/gpu/graphite/TextureProxyView.h
@@ -89,10 +89,14 @@
         return std::move(fProxy);
     }
 
+    // This Copy does not perform any copy-as-draw fallbacks; if 'srcView' does not support reading
+    // pixels, then this will return an empty proxy view. On success, the proxy view will be
+    // sampleable.
     static TextureProxyView Copy(Recorder*,
                                  const SkColorInfo& srcColorInfo,
                                  const TextureProxyView& srcView,
                                  SkIRect srcRect,
+                                 Budgeted,
                                  Mipmapped,
                                  SkBackingFit);
 
diff --git a/src/gpu/graphite/TextureUtils.h b/src/gpu/graphite/TextureUtils.h
index 0af6d0f..28f4d92 100644
--- a/src/gpu/graphite/TextureUtils.h
+++ b/src/gpu/graphite/TextureUtils.h
@@ -58,6 +58,7 @@
 
 // Returns the underlying TextureProxyView if it's a non-YUVA Graphite-backed image.
 TextureProxyView AsView(const SkImage*);
+inline TextureProxyView AsView(sk_sp<SkImage> image) { return AsView(image.get()); }
 
 std::pair<sk_sp<SkImage>, SkSamplingOptions> GetGraphiteBacked(Recorder*,
                                                                const SkImage*,
diff --git a/tests/FilterResultTest.cpp b/tests/FilterResultTest.cpp
index 41f8724..3685952 100644
--- a/tests/FilterResultTest.cpp
+++ b/tests/FilterResultTest.cpp
@@ -880,7 +880,7 @@
         if (fRecorder) {
             // Graphite backed, so use the private testing-only synchronous API
             SkASSERT(specialImage->isGraphiteBacked());
-            auto view = SkSpecialImages::AsTextureProxyView(specialImage);
+            auto view = skgpu::graphite::AsView(specialImage->asImage());
             auto proxyII = ii.makeWH(view.width(), view.height());
             SkAssertResult(fRecorder->priv().context()->priv().readPixels(
                     bm.pixmap(), view.proxy(), proxyII, srcX, srcY));