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