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