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