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