xref: /aosp_15_r20/external/skia/src/gpu/TiledTextureUtils.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2023 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/gpu/TiledTextureUtils.h"
9 
10 #include "include/core/SkBitmap.h"
11 #include "include/core/SkColor.h"
12 #include "include/core/SkMatrix.h"
13 #include "include/core/SkRect.h"
14 #include "include/core/SkSamplingOptions.h"
15 #include "include/core/SkSize.h"
16 #include "src/base/SkSafeMath.h"
17 #include "src/core/SkCanvasPriv.h"
18 #include "src/core/SkDevice.h"
19 #include "src/core/SkImagePriv.h"
20 #include "src/core/SkSamplingPriv.h"
21 #include "src/image/SkImage_Base.h"
22 #include "src/image/SkImage_Picture.h"
23 
24 #include <functional>
25 
26 //////////////////////////////////////////////////////////////////////////////
27 //  Helper functions for tiling a large SkBitmap
28 
29 namespace {
30 
31 static const int kBmpSmallTileSize = 1 << 10;
32 
get_tile_count(const SkIRect & srcRect,int tileSize)33 size_t get_tile_count(const SkIRect& srcRect, int tileSize)  {
34     int tilesX = (srcRect.fRight / tileSize) - (srcRect.fLeft / tileSize) + 1;
35     int tilesY = (srcRect.fBottom / tileSize) - (srcRect.fTop / tileSize) + 1;
36     // We calculate expected tile count before we read the bitmap's pixels, so hypothetically we can
37     // have lazy images with excessive dimensions that would cause (tilesX*tilesY) to overflow int.
38     // In these situations we also later fail to allocate a bitmap to store the lazy image, so there
39     // isn't really a performance concern around one image turning into millions of tiles.
40     return SkSafeMath::Mul(tilesX, tilesY);
41 }
42 
determine_tile_size(const SkIRect & src,int maxTileSize)43 int determine_tile_size(const SkIRect& src, int maxTileSize) {
44     if (maxTileSize <= kBmpSmallTileSize) {
45         return maxTileSize;
46     }
47 
48     size_t maxTileTotalTileSize = get_tile_count(src, maxTileSize);
49     size_t smallTotalTileSize = get_tile_count(src, kBmpSmallTileSize);
50 
51     maxTileTotalTileSize *= maxTileSize * maxTileSize;
52     smallTotalTileSize *= kBmpSmallTileSize * kBmpSmallTileSize;
53 
54     if (maxTileTotalTileSize > 2 * smallTotalTileSize) {
55         return kBmpSmallTileSize;
56     } else {
57         return maxTileSize;
58     }
59 }
60 
61 // Given a bitmap, an optional src rect, and a context with a clip and matrix determine what
62 // pixels from the bitmap are necessary.
determine_clipped_src_rect(SkIRect clippedSrcIRect,const SkMatrix & viewMatrix,const SkMatrix & srcToDstRect,const SkISize & imageDimensions,const SkRect * srcRectPtr)63 SkIRect determine_clipped_src_rect(SkIRect clippedSrcIRect,
64                                    const SkMatrix& viewMatrix,
65                                    const SkMatrix& srcToDstRect,
66                                    const SkISize& imageDimensions,
67                                    const SkRect* srcRectPtr) {
68     SkMatrix inv = SkMatrix::Concat(viewMatrix, srcToDstRect);
69     if (!inv.invert(&inv)) {
70         return SkIRect::MakeEmpty();
71     }
72     SkRect clippedSrcRect = SkRect::Make(clippedSrcIRect);
73     inv.mapRect(&clippedSrcRect);
74     if (srcRectPtr) {
75         if (!clippedSrcRect.intersect(*srcRectPtr)) {
76             return SkIRect::MakeEmpty();
77         }
78     }
79     clippedSrcRect.roundOut(&clippedSrcIRect);
80     SkIRect bmpBounds = SkIRect::MakeSize(imageDimensions);
81     if (!clippedSrcIRect.intersect(bmpBounds)) {
82         return SkIRect::MakeEmpty();
83     }
84 
85     return clippedSrcIRect;
86 }
87 
draw_tiled_image(SkCanvas * canvas,std::function<sk_sp<SkImage> (SkIRect)> imageProc,SkISize originalSize,int tileSize,const SkMatrix & srcToDst,const SkRect & srcRect,const SkIRect & clippedSrcIRect,const SkPaint * paint,SkCanvas::QuadAAFlags origAAFlags,SkCanvas::SrcRectConstraint constraint,SkSamplingOptions sampling)88 int draw_tiled_image(SkCanvas* canvas,
89                      std::function<sk_sp<SkImage>(SkIRect)> imageProc,
90                      SkISize originalSize,
91                      int tileSize,
92                      const SkMatrix& srcToDst,
93                      const SkRect& srcRect,
94                      const SkIRect& clippedSrcIRect,
95                      const SkPaint* paint,
96                      SkCanvas::QuadAAFlags origAAFlags,
97                      SkCanvas::SrcRectConstraint constraint,
98                      SkSamplingOptions sampling) {
99     if (sampling.isAniso()) {
100         sampling = SkSamplingPriv::AnisoFallback(/* imageIsMipped= */ false);
101     }
102     SkRect clippedSrcRect = SkRect::Make(clippedSrcIRect);
103 
104     int nx = originalSize.width() / tileSize;
105     int ny = originalSize.height() / tileSize;
106 
107     int numTilesDrawn = 0;
108 
109     skia_private::TArray<SkCanvas::ImageSetEntry> imgSet(nx * ny);
110 
111     for (int x = 0; x <= nx; x++) {
112         for (int y = 0; y <= ny; y++) {
113             SkRect tileR;
114             tileR.setLTRB(SkIntToScalar(x * tileSize),       SkIntToScalar(y * tileSize),
115                           SkIntToScalar((x + 1) * tileSize), SkIntToScalar((y + 1) * tileSize));
116 
117             if (!SkRect::Intersects(tileR, clippedSrcRect)) {
118                 continue;
119             }
120 
121             if (!tileR.intersect(srcRect)) {
122                 continue;
123             }
124 
125             SkIRect iTileR;
126             tileR.roundOut(&iTileR);
127             SkVector offset = SkPoint::Make(SkIntToScalar(iTileR.fLeft),
128                                             SkIntToScalar(iTileR.fTop));
129             SkRect rectToDraw = tileR;
130             if (!srcToDst.mapRect(&rectToDraw)) {
131                 continue;
132             }
133 
134             if (sampling.filter != SkFilterMode::kNearest || sampling.useCubic) {
135                 SkIRect iClampRect;
136 
137                 if (SkCanvas::kFast_SrcRectConstraint == constraint) {
138                     // In bleed mode we want to always expand the tile on all edges
139                     // but stay within the bitmap bounds
140                     iClampRect = SkIRect::MakeWH(originalSize.width(), originalSize.height());
141                 } else {
142                     // In texture-domain/clamp mode we only want to expand the
143                     // tile on edges interior to "srcRect" (i.e., we want to
144                     // not bleed across the original clamped edges)
145                     srcRect.roundOut(&iClampRect);
146                 }
147                 int outset = sampling.useCubic ? kBicubicFilterTexelPad : 1;
148                 skgpu::TiledTextureUtils::ClampedOutsetWithOffset(&iTileR, outset, &offset,
149                                                                   iClampRect);
150             }
151 
152             sk_sp<SkImage> image = imageProc(iTileR);
153             if (!image) {
154                 continue;
155             }
156 
157             unsigned aaFlags = SkCanvas::kNone_QuadAAFlags;
158             // Preserve the original edge AA flags for the exterior tile edges.
159             if (tileR.fLeft <= srcRect.fLeft && (origAAFlags & SkCanvas::kLeft_QuadAAFlag)) {
160                 aaFlags |= SkCanvas::kLeft_QuadAAFlag;
161             }
162             if (tileR.fRight >= srcRect.fRight && (origAAFlags & SkCanvas::kRight_QuadAAFlag)) {
163                 aaFlags |= SkCanvas::kRight_QuadAAFlag;
164             }
165             if (tileR.fTop <= srcRect.fTop && (origAAFlags & SkCanvas::kTop_QuadAAFlag)) {
166                 aaFlags |= SkCanvas::kTop_QuadAAFlag;
167             }
168             if (tileR.fBottom >= srcRect.fBottom &&
169                 (origAAFlags & SkCanvas::kBottom_QuadAAFlag)) {
170                 aaFlags |= SkCanvas::kBottom_QuadAAFlag;
171             }
172 
173             // Offset the source rect to make it "local" to our tmp bitmap
174             tileR.offset(-offset.fX, -offset.fY);
175 
176             imgSet.push_back(SkCanvas::ImageSetEntry(std::move(image),
177                                                      tileR,
178                                                      rectToDraw,
179                                                      /* matrixIndex= */ -1,
180                                                      /* alpha= */ 1.0f,
181                                                      aaFlags,
182                                                      /* hasClip= */ false));
183 
184             numTilesDrawn += 1;
185         }
186     }
187 
188     canvas->experimental_DrawEdgeAAImageSet(imgSet.data(),
189                                             imgSet.size(),
190                                             /* dstClips= */ nullptr,
191                                             /* preViewMatrices= */ nullptr,
192                                             sampling,
193                                             paint,
194                                             constraint);
195     return numTilesDrawn;
196 }
197 
198 } // anonymous namespace
199 
200 namespace skgpu {
201 
202 // tileSize and clippedSubset are valid if true is returned
ShouldTileImage(SkIRect conservativeClipBounds,const SkISize & imageSize,const SkMatrix & ctm,const SkMatrix & srcToDst,const SkRect * src,int maxTileSize,size_t cacheSize,int * tileSize,SkIRect * clippedSubset)203 bool TiledTextureUtils::ShouldTileImage(SkIRect conservativeClipBounds,
204                                         const SkISize& imageSize,
205                                         const SkMatrix& ctm,
206                                         const SkMatrix& srcToDst,
207                                         const SkRect* src,
208                                         int maxTileSize,
209                                         size_t cacheSize,
210                                         int* tileSize,
211                                         SkIRect* clippedSubset) {
212     // if it's larger than the max tile size, then we have no choice but tiling.
213     if (imageSize.width() > maxTileSize || imageSize.height() > maxTileSize) {
214         *clippedSubset = determine_clipped_src_rect(conservativeClipBounds, ctm,
215                                                     srcToDst, imageSize, src);
216         *tileSize = determine_tile_size(*clippedSubset, maxTileSize);
217         return true;
218     }
219 
220     // If the image would only produce 4 tiles of the smaller size, don't bother tiling it.
221     const size_t area = imageSize.width() * imageSize.height();
222     if (area < 4 * kBmpSmallTileSize * kBmpSmallTileSize) {
223         return false;
224     }
225 
226     // At this point we know we could do the draw by uploading the entire bitmap as a texture.
227     // However, if the texture would be large compared to the cache size and we don't require most
228     // of it for this draw then tile to reduce the amount of upload and cache spill.
229     if (!cacheSize) {
230         // We don't have access to the cacheSize so we will just upload the entire image
231         // to be on the safe side and not tile.
232         return false;
233     }
234 
235     // An assumption here is that sw bitmap size is a good proxy for its size as a texture
236     size_t bmpSize = area * sizeof(SkPMColor);  // assume 32bit pixels
237     if (bmpSize < cacheSize / 2) {
238         return false;
239     }
240 
241     // Figure out how much of the src we will need based on the src rect and clipping. Reject if
242     // tiling memory savings would be < 50%.
243     *clippedSubset = determine_clipped_src_rect(conservativeClipBounds, ctm,
244                                                 srcToDst, imageSize, src);
245     *tileSize = kBmpSmallTileSize; // already know whole bitmap fits in one max sized tile.
246     size_t usedTileBytes = get_tile_count(*clippedSubset, kBmpSmallTileSize) *
247                            kBmpSmallTileSize * kBmpSmallTileSize *
248                            sizeof(SkPMColor);  // assume 32bit pixels;
249 
250     return usedTileBytes * 2 < bmpSize;
251 }
252 
253 /**
254  * Optimize the src rect sampling area within an image (sized 'width' x 'height') such that
255  * 'outSrcRect' will be completely contained in the image's bounds. The corresponding rect
256  * to draw will be output to 'outDstRect'. The mapping between src and dst will be cached in
257  * 'outSrcToDst'. Outputs are not always updated when kSkip is returned.
258  *
259  * 'dstClip' should be null when there is no additional clipping.
260  */
OptimizeSampleArea(const SkISize & imageSize,const SkRect & origSrcRect,const SkRect & origDstRect,const SkPoint dstClip[4],SkRect * outSrcRect,SkRect * outDstRect,SkMatrix * outSrcToDst)261 TiledTextureUtils::ImageDrawMode TiledTextureUtils::OptimizeSampleArea(const SkISize& imageSize,
262                                                                        const SkRect& origSrcRect,
263                                                                        const SkRect& origDstRect,
264                                                                        const SkPoint dstClip[4],
265                                                                        SkRect* outSrcRect,
266                                                                        SkRect* outDstRect,
267                                                                        SkMatrix* outSrcToDst) {
268     if (origSrcRect.isEmpty() || origDstRect.isEmpty()) {
269         return ImageDrawMode::kSkip;
270     }
271 
272     *outSrcToDst = SkMatrix::RectToRect(origSrcRect, origDstRect);
273 
274     SkRect src = origSrcRect;
275     SkRect dst = origDstRect;
276 
277     const SkRect srcBounds = SkRect::Make(imageSize);
278 
279     if (!srcBounds.contains(src)) {
280         if (!src.intersect(srcBounds)) {
281             return ImageDrawMode::kSkip;
282         }
283         outSrcToDst->mapRect(&dst, src);
284 
285         // Both src and dst have gotten smaller. If dstClip is provided, confirm it is still
286         // contained in dst, otherwise cannot optimize the sample area and must use a decal instead
287         if (dstClip) {
288             for (int i = 0; i < 4; ++i) {
289                 if (!dst.contains(dstClip[i].fX, dstClip[i].fY)) {
290                     // Must resort to using a decal mode restricted to the clipped 'src', and
291                     // use the original dst rect (filling in src bounds as needed)
292                     *outSrcRect = src;
293                     *outDstRect = origDstRect;
294                     return ImageDrawMode::kDecal;
295                 }
296             }
297         }
298     }
299 
300     // The original src and dst were fully contained in the image, or there was no dst clip to
301     // worry about, or the clip was still contained in the restricted dst rect.
302     *outSrcRect = src;
303     *outDstRect = dst;
304     return ImageDrawMode::kOptimized;
305 }
306 
CanDisableMipmap(const SkMatrix & viewM,const SkMatrix & localM,bool sharpenMipmappedTextures)307 bool TiledTextureUtils::CanDisableMipmap(const SkMatrix& viewM,
308                                          const SkMatrix& localM,
309                                          bool sharpenMipmappedTextures) {
310     SkMatrix matrix;
311     matrix.setConcat(viewM, localM);
312     // With sharp mips, we bias mipmap lookups by -0.5. That means our final LOD is >= 0 until
313     // the computed LOD is >= 0.5. At what scale factor does a texture get an LOD of
314     // 0.5?
315     //
316     // Want:  0       = log2(1/s) - 0.5
317     //        0.5     = log2(1/s)
318     //        2^0.5   = 1/s
319     //        1/2^0.5 = s
320     //        2^0.5/2 = s
321     SkScalar mipScale = sharpenMipmappedTextures ? SK_ScalarRoot2Over2 : SK_Scalar1;
322     return matrix.getMinScale() >= mipScale;
323 }
324 
325 
326 // This method outsets 'iRect' by 'outset' all around and then clamps its extents to
327 // 'clamp'. 'offset' is adjusted to remain positioned over the top-left corner
328 // of 'iRect' for all possible outsets/clamps.
ClampedOutsetWithOffset(SkIRect * iRect,int outset,SkPoint * offset,const SkIRect & clamp)329 void TiledTextureUtils::ClampedOutsetWithOffset(SkIRect* iRect, int outset, SkPoint* offset,
330                                                 const SkIRect& clamp) {
331     iRect->outset(outset, outset);
332 
333     int leftClampDelta = clamp.fLeft - iRect->fLeft;
334     if (leftClampDelta > 0) {
335         offset->fX -= outset - leftClampDelta;
336         iRect->fLeft = clamp.fLeft;
337     } else {
338         offset->fX -= outset;
339     }
340 
341     int topClampDelta = clamp.fTop - iRect->fTop;
342     if (topClampDelta > 0) {
343         offset->fY -= outset - topClampDelta;
344         iRect->fTop = clamp.fTop;
345     } else {
346         offset->fY -= outset;
347     }
348 
349     if (iRect->fRight > clamp.fRight) {
350         iRect->fRight = clamp.fRight;
351     }
352     if (iRect->fBottom > clamp.fBottom) {
353         iRect->fBottom = clamp.fBottom;
354     }
355 }
356 
DrawAsTiledImageRect(SkCanvas * canvas,const SkImage * image,const SkRect & srcRect,const SkRect & dstRect,SkCanvas::QuadAAFlags aaFlags,const SkSamplingOptions & origSampling,const SkPaint * paint,SkCanvas::SrcRectConstraint constraint,bool sharpenMM,size_t cacheSize,size_t maxTextureSize)357 std::tuple<bool, size_t> TiledTextureUtils::DrawAsTiledImageRect(
358         SkCanvas* canvas,
359         const SkImage* image,
360         const SkRect& srcRect,
361         const SkRect& dstRect,
362         SkCanvas::QuadAAFlags aaFlags,
363         const SkSamplingOptions& origSampling,
364         const SkPaint* paint,
365         SkCanvas::SrcRectConstraint constraint,
366         bool sharpenMM,
367         size_t cacheSize,
368         size_t maxTextureSize) {
369     if (canvas->isClipEmpty()) {
370         return {true, 0};
371     }
372 
373     if (!image->isTextureBacked()) {
374         SkRect src;
375         SkRect dst;
376         SkMatrix srcToDst;
377         ImageDrawMode mode = OptimizeSampleArea(SkISize::Make(image->width(), image->height()),
378                                                 srcRect, dstRect, /* dstClip= */ nullptr,
379                                                 &src, &dst, &srcToDst);
380         if (mode == ImageDrawMode::kSkip) {
381             return {true, 0};
382         }
383 
384         SkASSERT(mode != ImageDrawMode::kDecal); // only happens if there is a 'dstClip'
385 
386         if (src.contains(image->bounds())) {
387             constraint = SkCanvas::kFast_SrcRectConstraint;
388         }
389 
390         SkDevice* device = SkCanvasPriv::TopDevice(canvas);
391         const SkMatrix& localToDevice = device->localToDevice();
392 
393         SkSamplingOptions sampling = origSampling;
394         if (sampling.mipmap != SkMipmapMode::kNone &&
395             CanDisableMipmap(localToDevice, srcToDst, sharpenMM)) {
396             sampling = SkSamplingOptions(sampling.filter);
397         }
398 
399         SkIRect clipRect = device->devClipBounds();
400 
401         int tileFilterPad;
402         if (sampling.useCubic) {
403             tileFilterPad = kBicubicFilterTexelPad;
404         } else if (sampling.filter == SkFilterMode::kLinear || sampling.isAniso()) {
405             // Aniso will fallback to linear filtering in the tiling case.
406             tileFilterPad = 1;
407         } else {
408             tileFilterPad = 0;
409         }
410 
411         int maxTileSize = maxTextureSize - 2 * tileFilterPad;
412         int tileSize;
413         SkIRect clippedSubset;
414         if (ShouldTileImage(clipRect,
415                             image->dimensions(),
416                             localToDevice,
417                             srcToDst,
418                             &src,
419                             maxTileSize,
420                             cacheSize,
421                             &tileSize,
422                             &clippedSubset)) {
423             // If it's a Picture-backed image we should subset the SkPicture directly rather than
424             // converting to a Bitmap and then subsetting. Rendering to a bitmap will use a Raster
425             // surface, and the SkPicture could have GPU data.
426             if (as_IB(image)->type() == SkImage_Base::Type::kLazyPicture) {
427                 auto imageProc = [&](SkIRect iTileR) {
428                     return image->makeSubset(nullptr, iTileR);
429                 };
430 
431                 size_t tiles = draw_tiled_image(canvas,
432                                                 imageProc,
433                                                 image->dimensions(),
434                                                 tileSize,
435                                                 srcToDst,
436                                                 src,
437                                                 clippedSubset,
438                                                 paint,
439                                                 aaFlags,
440                                                 constraint,
441                                                 sampling);
442                 return {true, tiles};
443             }
444 
445             // Extract pixels on the CPU, since we have to split into separate textures before
446             // sending to the GPU if tiling.
447             if (SkBitmap bm; as_IB(image)->getROPixels(nullptr, &bm)) {
448                 auto imageProc = [&](SkIRect iTileR) {
449                     // We must subset as a bitmap and then turn it into an SkImage if we want
450                     // caching to work. Image subsets always make a copy of the pixels and lose
451                     // the association with the original's SkPixelRef.
452                     if (SkBitmap subsetBmp; bm.extractSubset(&subsetBmp, iTileR)) {
453                         return SkMakeImageFromRasterBitmap(subsetBmp, kNever_SkCopyPixelsMode);
454                     }
455                     return sk_sp<SkImage>(nullptr);
456                 };
457 
458                 size_t tiles = draw_tiled_image(canvas,
459                                                 imageProc,
460                                                 bm.dimensions(),
461                                                 tileSize,
462                                                 srcToDst,
463                                                 src,
464                                                 clippedSubset,
465                                                 paint,
466                                                 aaFlags,
467                                                 constraint,
468                                                 sampling);
469                 return {true, tiles};
470             }
471         }
472     }
473 
474     return {false, 0};
475 }
476 
477 } // namespace skgpu
478