• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             // TODO: this will prevent int overflow, however at sizes > 2^24 the float can't
115             // represent all the bits in the int
116             int tileRight = (x == nx) ? originalSize.width() : (x + 1) * tileSize;
117             int tileBottom = (y == ny) ? originalSize.height() : (y + 1) * tileSize;
118             tileR.setLTRB(SkIntToScalar(x * tileSize), SkIntToScalar(y * tileSize),
119                           SkIntToScalar(tileRight),    SkIntToScalar(tileBottom));
120 
121             if (!SkRect::Intersects(tileR, clippedSrcRect)) {
122                 continue;
123             }
124 
125             if (!tileR.intersect(srcRect)) {
126                 continue;
127             }
128 
129             SkIRect iTileR;
130             tileR.roundOut(&iTileR);
131             SkVector offset = SkPoint::Make(SkIntToScalar(iTileR.fLeft),
132                                             SkIntToScalar(iTileR.fTop));
133             SkRect rectToDraw = tileR;
134             if (!srcToDst.mapRect(&rectToDraw)) {
135                 continue;
136             }
137 
138             if (sampling.filter != SkFilterMode::kNearest || sampling.useCubic) {
139                 SkIRect iClampRect;
140 
141                 if (SkCanvas::kFast_SrcRectConstraint == constraint) {
142                     // In bleed mode we want to always expand the tile on all edges
143                     // but stay within the bitmap bounds
144                     iClampRect = SkIRect::MakeWH(originalSize.width(), originalSize.height());
145                 } else {
146                     // In texture-domain/clamp mode we only want to expand the
147                     // tile on edges interior to "srcRect" (i.e., we want to
148                     // not bleed across the original clamped edges)
149                     srcRect.roundOut(&iClampRect);
150                 }
151                 int outset = sampling.useCubic ? kBicubicFilterTexelPad : 1;
152                 skgpu::TiledTextureUtils::ClampedOutsetWithOffset(&iTileR, outset, &offset,
153                                                                   iClampRect);
154             }
155 
156             sk_sp<SkImage> image = imageProc(iTileR);
157             if (!image) {
158                 continue;
159             }
160 
161             unsigned aaFlags = SkCanvas::kNone_QuadAAFlags;
162             // Preserve the original edge AA flags for the exterior tile edges.
163             if (tileR.fLeft <= srcRect.fLeft && (origAAFlags & SkCanvas::kLeft_QuadAAFlag)) {
164                 aaFlags |= SkCanvas::kLeft_QuadAAFlag;
165             }
166             if (tileR.fRight >= srcRect.fRight && (origAAFlags & SkCanvas::kRight_QuadAAFlag)) {
167                 aaFlags |= SkCanvas::kRight_QuadAAFlag;
168             }
169             if (tileR.fTop <= srcRect.fTop && (origAAFlags & SkCanvas::kTop_QuadAAFlag)) {
170                 aaFlags |= SkCanvas::kTop_QuadAAFlag;
171             }
172             if (tileR.fBottom >= srcRect.fBottom &&
173                 (origAAFlags & SkCanvas::kBottom_QuadAAFlag)) {
174                 aaFlags |= SkCanvas::kBottom_QuadAAFlag;
175             }
176 
177             // Offset the source rect to make it "local" to our tmp bitmap
178             tileR.offset(-offset.fX, -offset.fY);
179 
180             imgSet.push_back(SkCanvas::ImageSetEntry(std::move(image),
181                                                      tileR,
182                                                      rectToDraw,
183                                                      /* matrixIndex= */ -1,
184                                                      /* alpha= */ 1.0f,
185                                                      aaFlags,
186                                                      /* hasClip= */ false));
187 
188             numTilesDrawn += 1;
189         }
190     }
191 
192     canvas->experimental_DrawEdgeAAImageSet(imgSet.data(),
193                                             imgSet.size(),
194                                             /* dstClips= */ nullptr,
195                                             /* preViewMatrices= */ nullptr,
196                                             sampling,
197                                             paint,
198                                             constraint);
199     return numTilesDrawn;
200 }
201 
202 } // anonymous namespace
203 
204 namespace skgpu {
205 
206 // 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)207 bool TiledTextureUtils::ShouldTileImage(SkIRect conservativeClipBounds,
208                                         const SkISize& imageSize,
209                                         const SkMatrix& ctm,
210                                         const SkMatrix& srcToDst,
211                                         const SkRect* src,
212                                         int maxTileSize,
213                                         size_t cacheSize,
214                                         int* tileSize,
215                                         SkIRect* clippedSubset) {
216     // if it's larger than the max tile size, then we have no choice but tiling.
217     if (imageSize.width() > maxTileSize || imageSize.height() > maxTileSize) {
218         *clippedSubset = determine_clipped_src_rect(conservativeClipBounds, ctm,
219                                                     srcToDst, imageSize, src);
220         *tileSize = determine_tile_size(*clippedSubset, maxTileSize);
221         return true;
222     }
223 
224     // If the image would only produce 4 tiles of the smaller size, don't bother tiling it.
225     const size_t area = imageSize.width() * imageSize.height();
226     if (area < 4 * kBmpSmallTileSize * kBmpSmallTileSize) {
227         return false;
228     }
229 
230     // At this point we know we could do the draw by uploading the entire bitmap as a texture.
231     // However, if the texture would be large compared to the cache size and we don't require most
232     // of it for this draw then tile to reduce the amount of upload and cache spill.
233     if (!cacheSize) {
234         // We don't have access to the cacheSize so we will just upload the entire image
235         // to be on the safe side and not tile.
236         return false;
237     }
238 
239     // An assumption here is that sw bitmap size is a good proxy for its size as a texture
240     size_t bmpSize = area * sizeof(SkPMColor);  // assume 32bit pixels
241     if (bmpSize < cacheSize / 2) {
242         return false;
243     }
244 
245     // Figure out how much of the src we will need based on the src rect and clipping. Reject if
246     // tiling memory savings would be < 50%.
247     *clippedSubset = determine_clipped_src_rect(conservativeClipBounds, ctm,
248                                                 srcToDst, imageSize, src);
249     *tileSize = kBmpSmallTileSize; // already know whole bitmap fits in one max sized tile.
250     size_t usedTileBytes = get_tile_count(*clippedSubset, kBmpSmallTileSize) *
251                            kBmpSmallTileSize * kBmpSmallTileSize *
252                            sizeof(SkPMColor);  // assume 32bit pixels;
253 
254     return usedTileBytes * 2 < bmpSize;
255 }
256 
257 /**
258  * Optimize the src rect sampling area within an image (sized 'width' x 'height') such that
259  * 'outSrcRect' will be completely contained in the image's bounds. The corresponding rect
260  * to draw will be output to 'outDstRect'. The mapping between src and dst will be cached in
261  * 'outSrcToDst'. Outputs are not always updated when kSkip is returned.
262  *
263  * 'dstClip' should be null when there is no additional clipping.
264  */
OptimizeSampleArea(const SkISize & imageSize,const SkRect & origSrcRect,const SkRect & origDstRect,const SkPoint dstClip[4],SkRect * outSrcRect,SkRect * outDstRect,SkMatrix * outSrcToDst)265 TiledTextureUtils::ImageDrawMode TiledTextureUtils::OptimizeSampleArea(const SkISize& imageSize,
266                                                                        const SkRect& origSrcRect,
267                                                                        const SkRect& origDstRect,
268                                                                        const SkPoint dstClip[4],
269                                                                        SkRect* outSrcRect,
270                                                                        SkRect* outDstRect,
271                                                                        SkMatrix* outSrcToDst) {
272     if (origSrcRect.isEmpty() || origDstRect.isEmpty()) {
273         return ImageDrawMode::kSkip;
274     }
275 
276     *outSrcToDst = SkMatrix::RectToRect(origSrcRect, origDstRect);
277 
278     SkRect src = origSrcRect;
279     SkRect dst = origDstRect;
280 
281     const SkRect srcBounds = SkRect::Make(imageSize);
282 
283     if (!srcBounds.contains(src)) {
284         if (!src.intersect(srcBounds)) {
285             return ImageDrawMode::kSkip;
286         }
287         outSrcToDst->mapRect(&dst, src);
288 
289         // Both src and dst have gotten smaller. If dstClip is provided, confirm it is still
290         // contained in dst, otherwise cannot optimize the sample area and must use a decal instead
291         if (dstClip) {
292             for (int i = 0; i < 4; ++i) {
293                 if (!dst.contains(dstClip[i].fX, dstClip[i].fY)) {
294                     // Must resort to using a decal mode restricted to the clipped 'src', and
295                     // use the original dst rect (filling in src bounds as needed)
296                     *outSrcRect = src;
297                     *outDstRect = origDstRect;
298                     return ImageDrawMode::kDecal;
299                 }
300             }
301         }
302     }
303 
304     // The original src and dst were fully contained in the image, or there was no dst clip to
305     // worry about, or the clip was still contained in the restricted dst rect.
306     *outSrcRect = src;
307     *outDstRect = dst;
308     return ImageDrawMode::kOptimized;
309 }
310 
CanDisableMipmap(const SkMatrix & viewM,const SkMatrix & localM,bool sharpenMipmappedTextures)311 bool TiledTextureUtils::CanDisableMipmap(const SkMatrix& viewM,
312                                          const SkMatrix& localM,
313                                          bool sharpenMipmappedTextures) {
314     SkMatrix matrix;
315     matrix.setConcat(viewM, localM);
316     // With sharp mips, we bias mipmap lookups by -0.5. That means our final LOD is >= 0 until
317     // the computed LOD is >= 0.5. At what scale factor does a texture get an LOD of
318     // 0.5?
319     //
320     // Want:  0       = log2(1/s) - 0.5
321     //        0.5     = log2(1/s)
322     //        2^0.5   = 1/s
323     //        1/2^0.5 = s
324     //        2^0.5/2 = s
325     SkScalar mipScale = sharpenMipmappedTextures ? SK_ScalarRoot2Over2 : SK_Scalar1;
326     return matrix.getMinScale() >= mipScale;
327 }
328 
329 
330 // This method outsets 'iRect' by 'outset' all around and then clamps its extents to
331 // 'clamp'. 'offset' is adjusted to remain positioned over the top-left corner
332 // of 'iRect' for all possible outsets/clamps.
ClampedOutsetWithOffset(SkIRect * iRect,int outset,SkPoint * offset,const SkIRect & clamp)333 void TiledTextureUtils::ClampedOutsetWithOffset(SkIRect* iRect, int outset, SkPoint* offset,
334                                                 const SkIRect& clamp) {
335     iRect->outset(outset, outset);
336 
337     int leftClampDelta = clamp.fLeft - iRect->fLeft;
338     if (leftClampDelta > 0) {
339         offset->fX -= outset - leftClampDelta;
340         iRect->fLeft = clamp.fLeft;
341     } else {
342         offset->fX -= outset;
343     }
344 
345     int topClampDelta = clamp.fTop - iRect->fTop;
346     if (topClampDelta > 0) {
347         offset->fY -= outset - topClampDelta;
348         iRect->fTop = clamp.fTop;
349     } else {
350         offset->fY -= outset;
351     }
352 
353     if (iRect->fRight > clamp.fRight) {
354         iRect->fRight = clamp.fRight;
355     }
356     if (iRect->fBottom > clamp.fBottom) {
357         iRect->fBottom = clamp.fBottom;
358     }
359 }
360 
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)361 std::tuple<bool, size_t> TiledTextureUtils::DrawAsTiledImageRect(
362         SkCanvas* canvas,
363         const SkImage* image,
364         const SkRect& srcRect,
365         const SkRect& dstRect,
366         SkCanvas::QuadAAFlags aaFlags,
367         const SkSamplingOptions& origSampling,
368         const SkPaint* paint,
369         SkCanvas::SrcRectConstraint constraint,
370         bool sharpenMM,
371         size_t cacheSize,
372         size_t maxTextureSize) {
373     if (canvas->isClipEmpty()) {
374         return {true, 0};
375     }
376 
377     if (!image->isTextureBacked()) {
378         SkRect src;
379         SkRect dst;
380         SkMatrix srcToDst;
381         ImageDrawMode mode = OptimizeSampleArea(SkISize::Make(image->width(), image->height()),
382                                                 srcRect, dstRect, /* dstClip= */ nullptr,
383                                                 &src, &dst, &srcToDst);
384         if (mode == ImageDrawMode::kSkip) {
385             return {true, 0};
386         }
387 
388         SkASSERT(mode != ImageDrawMode::kDecal); // only happens if there is a 'dstClip'
389 
390         if (src.contains(image->bounds())) {
391             constraint = SkCanvas::kFast_SrcRectConstraint;
392         }
393 
394         SkDevice* device = SkCanvasPriv::TopDevice(canvas);
395         const SkMatrix& localToDevice = device->localToDevice();
396 
397         SkSamplingOptions sampling = origSampling;
398         if (sampling.mipmap != SkMipmapMode::kNone &&
399             CanDisableMipmap(localToDevice, srcToDst, sharpenMM)) {
400             sampling = SkSamplingOptions(sampling.filter);
401         }
402 
403         SkIRect clipRect = device->devClipBounds();
404 
405         int tileFilterPad;
406         if (sampling.useCubic) {
407             tileFilterPad = kBicubicFilterTexelPad;
408         } else if (sampling.filter == SkFilterMode::kLinear || sampling.isAniso()) {
409             // Aniso will fallback to linear filtering in the tiling case.
410             tileFilterPad = 1;
411         } else {
412             tileFilterPad = 0;
413         }
414 
415         int maxTileSize = maxTextureSize - 2 * tileFilterPad;
416         int tileSize;
417         SkIRect clippedSubset;
418         if (ShouldTileImage(clipRect,
419                             image->dimensions(),
420                             localToDevice,
421                             srcToDst,
422                             &src,
423                             maxTileSize,
424                             cacheSize,
425                             &tileSize,
426                             &clippedSubset)) {
427             // If it's a Picture-backed image we should subset the SkPicture directly rather than
428             // converting to a Bitmap and then subsetting. Rendering to a bitmap will use a Raster
429             // surface, and the SkPicture could have GPU data.
430             if (as_IB(image)->type() == SkImage_Base::Type::kLazyPicture) {
431                 auto imageProc = [&](SkIRect iTileR) {
432                     return image->makeSubset(nullptr, iTileR);
433                 };
434 
435                 size_t tiles = draw_tiled_image(canvas,
436                                                 imageProc,
437                                                 image->dimensions(),
438                                                 tileSize,
439                                                 srcToDst,
440                                                 src,
441                                                 clippedSubset,
442                                                 paint,
443                                                 aaFlags,
444                                                 constraint,
445                                                 sampling);
446                 return {true, tiles};
447             }
448 
449             // Extract pixels on the CPU, since we have to split into separate textures before
450             // sending to the GPU if tiling.
451             if (SkBitmap bm; as_IB(image)->getROPixels(nullptr, &bm)) {
452                 auto imageProc = [&](SkIRect iTileR) {
453                     // We must subset as a bitmap and then turn it into an SkImage if we want
454                     // caching to work. Image subsets always make a copy of the pixels and lose
455                     // the association with the original's SkPixelRef.
456                     if (SkBitmap subsetBmp; bm.extractSubset(&subsetBmp, iTileR)) {
457                         return SkMakeImageFromRasterBitmap(subsetBmp, kNever_SkCopyPixelsMode);
458                     }
459                     return sk_sp<SkImage>(nullptr);
460                 };
461 
462                 size_t tiles = draw_tiled_image(canvas,
463                                                 imageProc,
464                                                 bm.dimensions(),
465                                                 tileSize,
466                                                 srcToDst,
467                                                 src,
468                                                 clippedSubset,
469                                                 paint,
470                                                 aaFlags,
471                                                 constraint,
472                                                 sampling);
473                 return {true, tiles};
474             }
475         }
476     }
477 
478     return {false, 0};
479 }
480 
481 } // namespace skgpu
482