/* * Copyright 2019 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ // This test only works with the GPU backend. #include "gm/gm.h" #include "include/core/SkBitmap.h" #include "include/core/SkBlendMode.h" #include "include/core/SkCanvas.h" #include "include/core/SkColor.h" #include "include/core/SkColorFilter.h" #include "include/core/SkData.h" #include "include/core/SkFilterQuality.h" #include "include/core/SkFont.h" #include "include/core/SkImage.h" #include "include/core/SkImageFilter.h" #include "include/core/SkImageInfo.h" #include "include/core/SkMaskFilter.h" #include "include/core/SkMatrix.h" #include "include/core/SkPaint.h" #include "include/core/SkPoint.h" #include "include/core/SkRect.h" #include "include/core/SkRefCnt.h" #include "include/core/SkScalar.h" #include "include/core/SkShader.h" #include "include/core/SkSize.h" #include "include/core/SkString.h" #include "include/core/SkTileMode.h" #include "include/core/SkTypeface.h" #include "include/core/SkTypes.h" #include "include/effects/SkColorMatrix.h" #include "include/effects/SkGradientShader.h" #include "include/effects/SkImageFilters.h" #include "include/effects/SkShaderMaskFilter.h" #include "include/private/SkTArray.h" #include "src/core/SkLineClipper.h" #include "tools/Resources.h" #include "tools/gpu/YUVUtils.h" #include #include #include // This GM mimics the draw calls used by complex compositors that focus on drawing rectangles // and quadrilaterals with per-edge AA, with complex images, effects, and seamless tiling. // It will be updated to reflect the patterns seen in Chromium's SkiaRenderer. It is currently // restricted to adding draw ops directly in Ganesh since there is no fully-specified public API. static constexpr SkScalar kTileWidth = 40; static constexpr SkScalar kTileHeight = 30; static constexpr int kRowCount = 4; static constexpr int kColCount = 3; // To mimic Chromium's BSP clipping strategy, a set of three lines formed by triangle edges // of the below points are used to clip against the regular tile grid. The tile grid occupies // a 120 x 120 rectangle (40px * 3 cols by 30px * 4 rows). static constexpr SkPoint kClipP1 = {1.75f * kTileWidth, 0.8f * kTileHeight}; static constexpr SkPoint kClipP2 = {0.6f * kTileWidth, 2.f * kTileHeight}; static constexpr SkPoint kClipP3 = {2.9f * kTileWidth, 3.5f * kTileHeight}; /////////////////////////////////////////////////////////////////////////////////////////////// // Utilities for operating on lines and tiles /////////////////////////////////////////////////////////////////////////////////////////////// // p0 and p1 form a segment contained the tile grid, so extends them by a large enough margin // that the output points stored in 'line' are outside the tile grid (thus effectively infinite). static void clipping_line_segment(const SkPoint& p0, const SkPoint& p1, SkPoint line[2]) { SkVector v = p1 - p0; // 10f was chosen as a balance between large enough to scale the currently set clip // points outside of the tile grid, but small enough to preserve precision. line[0] = p0 - v * 10.f; line[1] = p1 + v * 10.f; } // Returns true if line segment (p0-p1) intersects with line segment (l0-l1); if true is returned, // the intersection point is stored in 'intersect'. static bool intersect_line_segments(const SkPoint& p0, const SkPoint& p1, const SkPoint& l0, const SkPoint& l1, SkPoint* intersect) { static constexpr SkScalar kHorizontalTolerance = 0.01f; // Pretty conservative // Use doubles for accuracy, since the clipping strategy used below can create T // junctions, and lower precision could artificially create gaps double pY = (double) p1.fY - (double) p0.fY; double pX = (double) p1.fX - (double) p0.fX; double lY = (double) l1.fY - (double) l0.fY; double lX = (double) l1.fX - (double) l0.fX; double plY = (double) p0.fY - (double) l0.fY; double plX = (double) p0.fX - (double) l0.fX; if (SkScalarNearlyZero(pY, kHorizontalTolerance)) { if (SkScalarNearlyZero(lY, kHorizontalTolerance)) { // Two horizontal lines return false; } else { // Recalculate but swap p and l return intersect_line_segments(l0, l1, p0, p1, intersect); } } // Up to now, the line segments do not form an invalid intersection double lNumerator = plX * pY - plY * pX; double lDenom = lX * pY - lY * pX; if (SkScalarNearlyZero(lDenom)) { // Parallel or identical return false; } // Calculate alphaL that provides the intersection point along (l0-l1), e.g. l0+alphaL*(l1-l0) double alphaL = lNumerator / lDenom; if (alphaL < 0.0 || alphaL > 1.0) { // Outside of the l segment return false; } // Calculate alphaP from the valid alphaL (since it could be outside p segment) // double alphaP = (alphaL * l.fY - pl.fY) / p.fY; double alphaP = (alphaL * lY - plY) / pY; if (alphaP < 0.0 || alphaP > 1.0) { // Outside of p segment return false; } // Is valid, so calculate the actual intersection point *intersect = l1 * SkScalar(alphaL) + l0 * SkScalar(1.0 - alphaL); return true; } // Draw a line through the two points, outset by a fixed length in screen space static void draw_outset_line(SkCanvas* canvas, const SkMatrix& local, const SkPoint pts[2], const SkPaint& paint) { static constexpr SkScalar kLineOutset = 10.f; SkPoint mapped[2]; local.mapPoints(mapped, pts, 2); SkVector v = mapped[1] - mapped[0]; v.setLength(v.length() + kLineOutset); canvas->drawLine(mapped[1] - v, mapped[0] + v, paint); } // Draw grid of red lines at interior tile boundaries. static void draw_tile_boundaries(SkCanvas* canvas, const SkMatrix& local) { SkPaint paint; paint.setAntiAlias(true); paint.setColor(SK_ColorRED); paint.setStyle(SkPaint::kStroke_Style); paint.setStrokeWidth(0.f); for (int x = 1; x < kColCount; ++x) { SkPoint pts[] = {{x * kTileWidth, 0}, {x * kTileWidth, kRowCount * kTileHeight}}; draw_outset_line(canvas, local, pts, paint); } for (int y = 1; y < kRowCount; ++y) { SkPoint pts[] = {{0, y * kTileHeight}, {kTileWidth * kColCount, y * kTileHeight}}; draw_outset_line(canvas, local, pts, paint); } } // Draw the arbitrary clipping/split boundaries that intersect the tile grid as green lines static void draw_clipping_boundaries(SkCanvas* canvas, const SkMatrix& local) { SkPaint paint; paint.setAntiAlias(true); paint.setColor(SK_ColorGREEN); paint.setStyle(SkPaint::kStroke_Style); paint.setStrokeWidth(0.f); // Clip the "infinite" line segments to a rectangular region outside the tile grid SkRect border = SkRect::MakeWH(kTileWidth * kColCount, kTileHeight * kRowCount); // Draw p1 to p2 SkPoint line[2]; SkPoint clippedLine[2]; clipping_line_segment(kClipP1, kClipP2, line); SkAssertResult(SkLineClipper::IntersectLine(line, border, clippedLine)); draw_outset_line(canvas, local, clippedLine, paint); // Draw p2 to p3 clipping_line_segment(kClipP2, kClipP3, line); SkAssertResult(SkLineClipper::IntersectLine(line, border, clippedLine)); draw_outset_line(canvas, local, clippedLine, paint); // Draw p3 to p1 clipping_line_segment(kClipP3, kClipP1, line); SkAssertResult(SkLineClipper::IntersectLine(line, border, clippedLine)); draw_outset_line(canvas, local, clippedLine, paint); } static void draw_text(SkCanvas* canvas, const char* text) { canvas->drawString(text, 0, 0, SkFont(nullptr, 12), SkPaint()); } ///////////////////////////////////////////////////////////////////////////////////////////////// // Abstraction for rendering a possibly clipped tile, that can apply different effects to mimic // the Chromium quad types, and a generic GM template to arrange renderers x transforms in a grid ///////////////////////////////////////////////////////////////////////////////////////////////// class ClipTileRenderer : public SkRefCntBase { public: virtual ~ClipTileRenderer() {} // Draw the base rect, possibly clipped by 'clip' if that is not null. The edges to antialias // are specified in 'edgeAA' (to make manipulation easier than an unsigned bitfield). 'tileID' // represents the location of rect within the tile grid, 'quadID' is the unique ID of the clip // region within the tile (reset for each tile). // // The edgeAA order matches that of clip, so it refers to top, right, bottom, left. // Return draw count virtual int drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4], int tileID, int quadID) = 0; virtual void drawBanner(SkCanvas* canvas) = 0; // Return draw count virtual int drawTiles(SkCanvas* canvas) { // All three lines in a list SkPoint lines[6]; clipping_line_segment(kClipP1, kClipP2, lines); clipping_line_segment(kClipP2, kClipP3, lines + 2); clipping_line_segment(kClipP3, kClipP1, lines + 4); bool edgeAA[4]; int tileID = 0; int drawCount = 0; for (int i = 0; i < kRowCount; ++i) { for (int j = 0; j < kColCount; ++j) { // The unclipped tile geometry SkRect tile = SkRect::MakeXYWH(j * kTileWidth, i * kTileHeight, kTileWidth, kTileHeight); // Base edge AA flags if there are no clips; clipped lines will only turn off edges edgeAA[0] = i == 0; // Top edgeAA[1] = j == kColCount - 1; // Right edgeAA[2] = i == kRowCount - 1; // Bottom edgeAA[3] = j == 0; // Left // Now clip against the 3 lines formed by kClipPx and split into general purpose // quads as needed. int quadCount = 0; drawCount += this->clipTile(canvas, tileID, tile, nullptr, edgeAA, lines, 3, &quadCount); tileID++; } } return drawCount; } protected: SkCanvas::QuadAAFlags maskToFlags(const bool edgeAA[4]) const { unsigned flags = (edgeAA[0] * SkCanvas::kTop_QuadAAFlag) | (edgeAA[1] * SkCanvas::kRight_QuadAAFlag) | (edgeAA[2] * SkCanvas::kBottom_QuadAAFlag) | (edgeAA[3] * SkCanvas::kLeft_QuadAAFlag); return static_cast(flags); } // Recursively splits the quadrilateral against the segments stored in 'lines', which must be // 2 * lineCount long. Increments 'quadCount' for each split quadrilateral, and invokes the // drawTile at leaves. int clipTile(SkCanvas* canvas, int tileID, const SkRect& baseRect, const SkPoint quad[4], const bool edgeAA[4], const SkPoint lines[], int lineCount, int* quadCount) { if (lineCount == 0) { // No lines, so end recursion by drawing the tile. If the tile was never split then // 'quad' remains null so that drawTile() can differentiate how it should draw. int draws = this->drawTile(canvas, baseRect, quad, edgeAA, tileID, *quadCount); *quadCount = *quadCount + 1; return draws; } static constexpr int kTL = 0; // Top-left point index in points array static constexpr int kTR = 1; // Top-right point index in points array static constexpr int kBR = 2; // Bottom-right point index in points array static constexpr int kBL = 3; // Bottom-left point index in points array static constexpr int kS0 = 4; // First split point index in points array static constexpr int kS1 = 5; // Second split point index in points array SkPoint points[6]; if (quad) { // Copy the original 4 points into set of points to consider for (int i = 0; i < 4; ++i) { points[i] = quad[i]; } } else { // Haven't been split yet, so fill in based on the rect baseRect.toQuad(points); } // Consider the first line against the 4 quad edges in tile, which should have 0,1, or 2 // intersection points since the tile is convex. int splitIndices[2]; // Edge that was intersected int intersectionCount = 0; for (int i = 0; i < 4; ++i) { SkPoint intersect; if (intersect_line_segments(points[i], points[i == 3 ? 0 : i + 1], lines[0], lines[1], &intersect)) { // If the intersected point is the same as the last found intersection, the line // runs through a vertex, so don't double count it bool duplicate = false; for (int j = 0; j < intersectionCount; ++j) { if (SkScalarNearlyZero((intersect - points[kS0 + j]).length())) { duplicate = true; break; } } if (!duplicate) { points[kS0 + intersectionCount] = intersect; splitIndices[intersectionCount] = i; intersectionCount++; } } } if (intersectionCount < 2) { // Either the first line never intersected the quad (count == 0), or it intersected at a // single vertex without going through quad area (count == 1), so check next line return this->clipTile( canvas, tileID, baseRect, quad, edgeAA, lines + 2, lineCount - 1, quadCount); } SkASSERT(intersectionCount == 2); // Split the tile points into 2+ sub quads and recurse to the next lines, which may or may // not further split the tile. Since the configurations are relatively simple, the possible // splits are hardcoded below; subtile quad orderings are such that the sub tiles remain in // clockwise order and match expected edges for QuadAAFlags. subtile indices refer to the // 6-element 'points' array. SkSTArray<3, std::array> subtiles; int s2 = -1; // Index of an original vertex chosen for a artificial split if (splitIndices[1] - splitIndices[0] == 2) { // Opposite edges, so the split trivially forms 2 sub quads if (splitIndices[0] == 0) { subtiles.push_back({{kTL, kS0, kS1, kBL}}); subtiles.push_back({{kS0, kTR, kBR, kS1}}); } else { subtiles.push_back({{kTL, kTR, kS0, kS1}}); subtiles.push_back({{kS1, kS0, kBR, kBL}}); } } else { // Adjacent edges, which makes for a more complicated split, since it forms a degenerate // quad (triangle) and a pentagon that must be artificially split. The pentagon is split // using one of the original vertices (remembered in 's2'), which adds an additional // degenerate quad, but ensures there are no T-junctions. switch(splitIndices[0]) { case 0: // Could be connected to edge 1 or edge 3 if (splitIndices[1] == 1) { s2 = kBL; subtiles.push_back({{kS0, kTR, kS1, kS0}}); // degenerate subtiles.push_back({{kTL, kS0, edgeAA[0] ? kS0 : kBL, kBL}}); // degenerate subtiles.push_back({{kS0, kS1, kBR, kBL}}); } else { SkASSERT(splitIndices[1] == 3); s2 = kBR; subtiles.push_back({{kTL, kS0, kS1, kS1}}); // degenerate subtiles.push_back({{kS1, edgeAA[3] ? kS1 : kBR, kBR, kBL}}); // degenerate subtiles.push_back({{kS0, kTR, kBR, kS1}}); } break; case 1: // Edge 0 handled above, should only be connected to edge 2 SkASSERT(splitIndices[1] == 2); s2 = kTL; subtiles.push_back({{kS0, kS0, kBR, kS1}}); // degenerate subtiles.push_back({{kTL, kTR, kS0, edgeAA[1] ? kS0 : kTL}}); // degenerate subtiles.push_back({{kTL, kS0, kS1, kBL}}); break; case 2: // Edge 1 handled above, should only be connected to edge 3 SkASSERT(splitIndices[1] == 3); s2 = kTR; subtiles.push_back({{kS1, kS0, kS0, kBL}}); // degenerate subtiles.push_back({{edgeAA[2] ? kS0 : kTR, kTR, kBR, kS0}}); // degenerate subtiles.push_back({{kTL, kTR, kS0, kS1}}); break; case 3: // Fall through, an adjacent edge split that hits edge 3 should have first found // been found with edge 0 or edge 2 for the other end default: SkASSERT(false); return 0; } } SkPoint sub[4]; bool subAA[4]; int draws = 0; for (int i = 0; i < subtiles.count(); ++i) { // Fill in the quad points and update edge AA rules for new interior edges for (int j = 0; j < 4; ++j) { int p = subtiles[i][j]; sub[j] = points[p]; int np = j == 3 ? subtiles[i][0] : subtiles[i][j + 1]; // The "new" edges are the edges that connect between the two split points or // between a split point and the chosen s2 point. Otherwise the edge remains aligned // with the original shape, so should preserve the AA setting. if ((p >= kS0 && (np == s2 || np >= kS0)) || ((np >= kS0) && (p == s2 || p >= kS0))) { // New edge subAA[j] = false; } else { // The subtiles indices were arranged so that their edge ordering was still top, // right, bottom, left so 'j' can be used to access edgeAA subAA[j] = edgeAA[j]; } } // Split the sub quad with the next line draws += this->clipTile(canvas, tileID, baseRect, sub, subAA, lines + 2, lineCount - 1, quadCount); } return draws; } }; static constexpr int kMatrixCount = 5; class CompositorGM : public skiagm::GM { public: CompositorGM(const char* name, sk_sp renderer) : fName(name) { fRenderers.push_back(std::move(renderer)); } CompositorGM(const char* name, const SkTArray> renderers) : fRenderers(renderers) , fName(name) {} protected: SkISize onISize() override { // The GM draws a grid of renderers (rows) x transforms (col). Within each cell, the // renderer draws the transformed tile grid, which is approximately // (kColCount*kTileWidth, kRowCount*kTileHeight), although it has additional line // visualizations and can be transformed outside of those rectangular bounds (i.e. persp), // so pad the cell dimensions to be conservative. Must also account for the banner text. static constexpr SkScalar kCellWidth = 1.3f * kColCount * kTileWidth; static constexpr SkScalar kCellHeight = 1.3f * kRowCount * kTileHeight; return SkISize::Make(SkScalarRoundToInt(kCellWidth * kMatrixCount + 175.f), SkScalarRoundToInt(kCellHeight * fRenderers.count() + 75.f)); } SkString onShortName() override { SkString fullName; fullName.appendf("compositor_quads_%s", fName.c_str()); return fullName; } void onOnceBeforeDraw() override { this->configureMatrices(); } void onDraw(SkCanvas* canvas) override { static constexpr SkScalar kGap = 40.f; static constexpr SkScalar kBannerWidth = 120.f; static constexpr SkScalar kOffset = 15.f; SkTArray drawCounts(fRenderers.count()); drawCounts.push_back_n(fRenderers.count(), 0); canvas->save(); canvas->translate(kOffset + kBannerWidth, kOffset); for (int i = 0; i < fMatrices.count(); ++i) { canvas->save(); draw_text(canvas, fMatrixNames[i].c_str()); canvas->translate(0.f, kGap); for (int j = 0; j < fRenderers.count(); ++j) { canvas->save(); draw_tile_boundaries(canvas, fMatrices[i]); draw_clipping_boundaries(canvas, fMatrices[i]); canvas->concat(fMatrices[i]); drawCounts[j] += fRenderers[j]->drawTiles(canvas); canvas->restore(); // And advance to the next row canvas->translate(0.f, kGap + kRowCount * kTileHeight); } // Reset back to the left edge canvas->restore(); // And advance to the next column canvas->translate(kGap + kColCount * kTileWidth, 0.f); } canvas->restore(); // Print a row header, with total draw counts canvas->save(); canvas->translate(kOffset, kGap + 0.5f * kRowCount * kTileHeight); for (int j = 0; j < fRenderers.count(); ++j) { fRenderers[j]->drawBanner(canvas); canvas->translate(0.f, 15.f); draw_text(canvas, SkStringPrintf("Draws = %d", drawCounts[j]).c_str()); canvas->translate(0.f, kGap + kRowCount * kTileHeight); } canvas->restore(); } private: SkTArray> fRenderers; SkTArray fMatrices; SkTArray fMatrixNames; SkString fName; void configureMatrices() { fMatrices.reset(); fMatrixNames.reset(); fMatrices.push_back_n(kMatrixCount); // Identity fMatrices[0].setIdentity(); fMatrixNames.push_back(SkString("Identity")); // Translate/scale fMatrices[1].setTranslate(5.5f, 20.25f); fMatrices[1].postScale(.9f, .7f); fMatrixNames.push_back(SkString("T+S")); // Rotation fMatrices[2].setRotate(20.0f); fMatrices[2].preTranslate(15.f, -20.f); fMatrixNames.push_back(SkString("Rotate")); // Skew fMatrices[3].setSkew(.5f, .25f); fMatrices[3].preTranslate(-30.f, 0.f); fMatrixNames.push_back(SkString("Skew")); // Perspective SkPoint src[4]; SkRect::MakeWH(kColCount * kTileWidth, kRowCount * kTileHeight).toQuad(src); SkPoint dst[4] = {{0, 0}, {kColCount * kTileWidth + 10.f, 15.f}, {kColCount * kTileWidth - 28.f, kRowCount * kTileHeight + 40.f}, {25.f, kRowCount * kTileHeight - 15.f}}; SkAssertResult(fMatrices[4].setPolyToPoly(src, dst, 4)); fMatrices[4].preTranslate(0.f, 10.f); fMatrixNames.push_back(SkString("Perspective")); SkASSERT(fMatrices.count() == fMatrixNames.count()); } typedef skiagm::GM INHERITED; }; //////////////////////////////////////////////////////////////////////////////////////////////// // Implementations of TileRenderer that color the clipped tiles in various ways //////////////////////////////////////////////////////////////////////////////////////////////// class DebugTileRenderer : public ClipTileRenderer { public: static sk_sp Make() { // Since aa override is disabled, the quad flags arg doesn't matter. return sk_sp(new DebugTileRenderer(SkCanvas::kAll_QuadAAFlags, false)); } static sk_sp MakeAA() { return sk_sp(new DebugTileRenderer(SkCanvas::kAll_QuadAAFlags, true)); } static sk_sp MakeNonAA() { return sk_sp(new DebugTileRenderer(SkCanvas::kNone_QuadAAFlags, true)); } int drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4], int tileID, int quadID) override { // Colorize the tile based on its grid position and quad ID int i = tileID / kColCount; int j = tileID % kColCount; SkColor4f c = {(i + 1.f) / kRowCount, (j + 1.f) / kColCount, .4f, 1.f}; float alpha = quadID / 10.f; c.fR = c.fR * (1 - alpha) + alpha; c.fG = c.fG * (1 - alpha) + alpha; c.fB = c.fB * (1 - alpha) + alpha; c.fA = c.fA * (1 - alpha) + alpha; SkCanvas::QuadAAFlags aaFlags = fEnableAAOverride ? fAAOverride : this->maskToFlags(edgeAA); canvas->experimental_DrawEdgeAAQuad( rect, clip, aaFlags, c.toSkColor(), SkBlendMode::kSrcOver); return 1; } void drawBanner(SkCanvas* canvas) override { draw_text(canvas, "Edge AA"); canvas->translate(0.f, 15.f); SkString config; static const char* kFormat = "Ext(%s) - Int(%s)"; if (fEnableAAOverride) { SkASSERT(fAAOverride == SkCanvas::kAll_QuadAAFlags || fAAOverride == SkCanvas::kNone_QuadAAFlags); if (fAAOverride == SkCanvas::kAll_QuadAAFlags) { config.appendf(kFormat, "yes", "yes"); } else { config.appendf(kFormat, "no", "no"); } } else { config.appendf(kFormat, "yes", "no"); } draw_text(canvas, config.c_str()); } private: SkCanvas::QuadAAFlags fAAOverride; bool fEnableAAOverride; DebugTileRenderer(SkCanvas::QuadAAFlags aa, bool enableAAOverrde) : fAAOverride(aa) , fEnableAAOverride(enableAAOverrde) {} typedef ClipTileRenderer INHERITED; }; // Tests tmp_drawEdgeAAQuad class SolidColorRenderer : public ClipTileRenderer { public: static sk_sp Make(const SkColor4f& color) { return sk_sp(new SolidColorRenderer(color)); } int drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4], int tileID, int quadID) override { canvas->experimental_DrawEdgeAAQuad(rect, clip, this->maskToFlags(edgeAA), fColor.toSkColor(), SkBlendMode::kSrcOver); return 1; } void drawBanner(SkCanvas* canvas) override { draw_text(canvas, "Solid Color"); } private: SkColor4f fColor; SolidColorRenderer(const SkColor4f& color) : fColor(color) {} typedef ClipTileRenderer INHERITED; }; // Tests drawEdgeAAImageSet(), but can batch the entries together in different ways class TextureSetRenderer : public ClipTileRenderer { public: static sk_sp MakeUnbatched(sk_sp image) { return Make("Texture", "", std::move(image), nullptr, nullptr, nullptr, nullptr, 1.f, true, 0); } static sk_sp MakeBatched(sk_sp image, int transformCount) { const char* subtitle = transformCount == 0 ? "" : "w/ xforms"; return Make("Texture Set", subtitle, std::move(image), nullptr, nullptr, nullptr, nullptr, 1.f, false, transformCount); } static sk_sp MakeShader(const char* name, sk_sp image, sk_sp shader, bool local) { return Make("Shader", name, std::move(image), std::move(shader), nullptr, nullptr, nullptr, 1.f, local, 0); } static sk_sp MakeColorFilter(const char* name, sk_sp image, sk_sp filter) { return Make("Color Filter", name, std::move(image), nullptr, std::move(filter), nullptr, nullptr, 1.f, false, 0); } static sk_sp MakeImageFilter(const char* name, sk_sp image, sk_sp filter) { return Make("Image Filter", name, std::move(image), nullptr, nullptr, std::move(filter), nullptr, 1.f, false, 0); } static sk_sp MakeMaskFilter(const char* name, sk_sp image, sk_sp filter) { return Make("Mask Filter", name, std::move(image), nullptr, nullptr, nullptr, std::move(filter), 1.f, false, 0); } static sk_sp MakeAlpha(sk_sp image, SkScalar alpha) { return Make("Alpha", SkStringPrintf("a = %.2f", alpha).c_str(), std::move(image), nullptr, nullptr, nullptr, nullptr, alpha, false, 0); } static sk_sp Make(const char* topBanner, const char* bottomBanner, sk_sp image, sk_sp shader, sk_sp colorFilter, sk_sp imageFilter, sk_sp maskFilter, SkScalar paintAlpha, bool resetAfterEachQuad, int transformCount) { return sk_sp(new TextureSetRenderer(topBanner, bottomBanner, std::move(image), std::move(shader), std::move(colorFilter), std::move(imageFilter), std::move(maskFilter), paintAlpha, resetAfterEachQuad, transformCount)); } int drawTiles(SkCanvas* canvas) override { int draws = this->INHERITED::drawTiles(canvas); // Push the last tile set draws += this->drawAndReset(canvas); return draws; } int drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4], int tileID, int quadID) override { // Now don't actually draw the tile, accumulate it in the growing entry set bool hasClip = false; if (clip) { // Record the four points into fDstClips fDstClips.push_back_n(4, clip); hasClip = true; } int matrixIdx = -1; if (!fResetEachQuad && fTransformBatchCount > 0) { // Handle transform batching. This works by capturing the CTM of the first tile draw, // and then calculate the difference between that and future CTMs for later tiles. if (fPreViewMatrices.count() == 0) { fBaseCTM = canvas->getTotalMatrix(); fPreViewMatrices.push_back(SkMatrix::I()); matrixIdx = 0; } else { // Calculate matrix s.t. getTotalMatrix() = fBaseCTM * M SkMatrix invBase; if (!fBaseCTM.invert(&invBase)) { SkDebugf("Cannot invert CTM, transform batching will not be correct.\n"); } else { SkMatrix preView = SkMatrix::Concat(invBase, canvas->getTotalMatrix()); if (preView != fPreViewMatrices[fPreViewMatrices.count() - 1]) { // Add the new matrix fPreViewMatrices.push_back(preView); } // else re-use the last matrix matrixIdx = fPreViewMatrices.count() - 1; } } } // This acts like the whole image is rendered over the entire tile grid, so derive local // coordinates from 'rect', based on the grid to image transform. SkMatrix gridToImage = SkMatrix::MakeRectToRect(SkRect::MakeWH(kColCount * kTileWidth, kRowCount * kTileHeight), SkRect::MakeWH(fImage->width(), fImage->height()), SkMatrix::kFill_ScaleToFit); SkRect localRect = gridToImage.mapRect(rect); // drawTextureSet automatically derives appropriate local quad from localRect if clipPtr // is not null. fSetEntries.push_back( {fImage, localRect, rect, matrixIdx, 1.f, this->maskToFlags(edgeAA), hasClip}); if (fResetEachQuad) { // Only ever draw one entry at a time return this->drawAndReset(canvas); } else { return 0; } } void drawBanner(SkCanvas* canvas) override { if (fTopBanner.size() > 0) { draw_text(canvas, fTopBanner.c_str()); } canvas->translate(0.f, 15.f); if (fBottomBanner.size() > 0) { draw_text(canvas, fBottomBanner.c_str()); } } private: SkString fTopBanner; SkString fBottomBanner; sk_sp fImage; sk_sp fShader; sk_sp fColorFilter; sk_sp fImageFilter; sk_sp fMaskFilter; SkScalar fPaintAlpha; // Batching rules bool fResetEachQuad; int fTransformBatchCount; SkTArray fDstClips; SkTArray fPreViewMatrices; SkTArray fSetEntries; SkMatrix fBaseCTM; int fBatchCount; TextureSetRenderer(const char* topBanner, const char* bottomBanner, sk_sp image, sk_sp shader, sk_sp colorFilter, sk_sp imageFilter, sk_sp maskFilter, SkScalar paintAlpha, bool resetEachQuad, int transformBatchCount) : fTopBanner(topBanner) , fBottomBanner(bottomBanner) , fImage(std::move(image)) , fShader(std::move(shader)) , fColorFilter(std::move(colorFilter)) , fImageFilter(std::move(imageFilter)) , fMaskFilter(std::move(maskFilter)) , fPaintAlpha(paintAlpha) , fResetEachQuad(resetEachQuad) , fTransformBatchCount(transformBatchCount) , fBatchCount(0) { SkASSERT(transformBatchCount >= 0 && (!resetEachQuad || transformBatchCount == 0)); } void configureTilePaint(const SkRect& rect, SkPaint* paint) const { paint->setAntiAlias(true); paint->setFilterQuality(kLow_SkFilterQuality); paint->setBlendMode(SkBlendMode::kSrcOver); // Send non-white RGB, that should be ignored paint->setColor4f({1.f, 0.4f, 0.25f, fPaintAlpha}, nullptr); if (fShader) { if (fResetEachQuad) { // Apply a local transform in the shader to map from the tile rectangle to (0,0,w,h) static const SkRect kTarget = SkRect::MakeWH(kTileWidth, kTileHeight); SkMatrix local = SkMatrix::MakeRectToRect(kTarget, rect, SkMatrix::kFill_ScaleToFit); paint->setShader(fShader->makeWithLocalMatrix(local)); } else { paint->setShader(fShader); } } paint->setColorFilter(fColorFilter); paint->setImageFilter(fImageFilter); paint->setMaskFilter(fMaskFilter); } int drawAndReset(SkCanvas* canvas) { // Early out if there's nothing to draw if (fSetEntries.count() == 0) { SkASSERT(fDstClips.count() == 0 && fPreViewMatrices.count() == 0); return 0; } if (!fResetEachQuad && fTransformBatchCount > 0) { // A batch is completed fBatchCount++; if (fBatchCount < fTransformBatchCount) { // Haven't hit the point to submit yet, but end the current tile return 0; } // Submitting all tiles back to where fBaseCTM was the canvas' matrix, while the // canvas currently has the CTM of the last tile batch, so reset it. canvas->setMatrix(fBaseCTM); } #ifdef SK_DEBUG int expectedDstClipCount = 0; for (int i = 0; i < fSetEntries.count(); ++i) { expectedDstClipCount += 4 * fSetEntries[i].fHasClip; SkASSERT(fSetEntries[i].fMatrixIndex < 0 || fSetEntries[i].fMatrixIndex < fPreViewMatrices.count()); } SkASSERT(expectedDstClipCount == fDstClips.count()); #endif SkPaint paint; SkRect lastTileRect = fSetEntries[fSetEntries.count() - 1].fDstRect; this->configureTilePaint(lastTileRect, &paint); canvas->experimental_DrawEdgeAAImageSet( fSetEntries.begin(), fSetEntries.count(), fDstClips.begin(), fPreViewMatrices.begin(), &paint, SkCanvas::kFast_SrcRectConstraint); // Reset for next tile fDstClips.reset(); fPreViewMatrices.reset(); fSetEntries.reset(); fBatchCount = 0; return 1; } typedef ClipTileRenderer INHERITED; }; class YUVTextureSetRenderer : public ClipTileRenderer { public: static sk_sp MakeFromJPEG(sk_sp imageData) { return sk_sp(new YUVTextureSetRenderer(std::move(imageData))); } int drawTiles(SkCanvas* canvas) override { // Refresh the SkImage at the start, so that it's not attempted for every set entry if (fYUVData) { fImage = fYUVData->refImage(canvas->getGrContext()); if (!fImage) { return 0; } } int draws = this->INHERITED::drawTiles(canvas); // Push the last tile set draws += this->drawAndReset(canvas); return draws; } int drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4], int tileID, int quadID) override { SkASSERT(fImage); // Now don't actually draw the tile, accumulate it in the growing entry set bool hasClip = false; if (clip) { // Record the four points into fDstClips fDstClips.push_back_n(4, clip); hasClip = true; } // This acts like the whole image is rendered over the entire tile grid, so derive local // coordinates from 'rect', based on the grid to image transform. SkMatrix gridToImage = SkMatrix::MakeRectToRect(SkRect::MakeWH(kColCount * kTileWidth, kRowCount * kTileHeight), SkRect::MakeWH(fImage->width(), fImage->height()), SkMatrix::kFill_ScaleToFit); SkRect localRect = gridToImage.mapRect(rect); // drawTextureSet automatically derives appropriate local quad from localRect if clipPtr // is not null. Also exercise per-entry alpha combined with YUVA images. fSetEntries.push_back( {fImage, localRect, rect, -1, .5f, this->maskToFlags(edgeAA), hasClip}); return 0; } void drawBanner(SkCanvas* canvas) override { draw_text(canvas, "Texture"); canvas->translate(0.f, 15.f); draw_text(canvas, "YUV + alpha - GPU Only"); } private: std::unique_ptr fYUVData; // The last accessed SkImage from fYUVData, held here for easy access by drawTile sk_sp fImage; SkTArray fDstClips; SkTArray fSetEntries; YUVTextureSetRenderer(sk_sp jpegData) : fYUVData(sk_gpu_test::LazyYUVImage::Make(std::move(jpegData))) , fImage(nullptr) {} int drawAndReset(SkCanvas* canvas) { // Early out if there's nothing to draw if (fSetEntries.count() == 0) { SkASSERT(fDstClips.count() == 0); return 0; } #ifdef SK_DEBUG int expectedDstClipCount = 0; for (int i = 0; i < fSetEntries.count(); ++i) { expectedDstClipCount += 4 * fSetEntries[i].fHasClip; } SkASSERT(expectedDstClipCount == fDstClips.count()); #endif SkPaint paint; paint.setAntiAlias(true); paint.setFilterQuality(kLow_SkFilterQuality); paint.setBlendMode(SkBlendMode::kSrcOver); canvas->experimental_DrawEdgeAAImageSet( fSetEntries.begin(), fSetEntries.count(), fDstClips.begin(), nullptr, &paint, SkCanvas::kFast_SrcRectConstraint); // Reset for next tile fDstClips.reset(); fSetEntries.reset(); return 1; } typedef ClipTileRenderer INHERITED; }; static SkTArray> make_debug_renderers() { SkTArray> renderers; renderers.push_back(DebugTileRenderer::Make()); renderers.push_back(DebugTileRenderer::MakeAA()); renderers.push_back(DebugTileRenderer::MakeNonAA()); return renderers; } static SkTArray> make_shader_renderers() { static constexpr SkPoint kPts[] = { {0.f, 0.f}, {0.25f * kTileWidth, 0.25f * kTileHeight} }; static constexpr SkColor kColors[] = { SK_ColorBLUE, SK_ColorWHITE }; auto gradient = SkGradientShader::MakeLinear(kPts, kColors, nullptr, 2, SkTileMode::kMirror); auto info = SkImageInfo::Make(1, 1, kAlpha_8_SkColorType, kOpaque_SkAlphaType); SkBitmap bm; bm.allocPixels(info); bm.eraseColor(SK_ColorWHITE); sk_sp image = SkImage::MakeFromBitmap(bm); SkTArray> renderers; renderers.push_back(TextureSetRenderer::MakeShader("Gradient", image, gradient, false)); renderers.push_back(TextureSetRenderer::MakeShader("Local Gradient", image, gradient, true)); return renderers; } static SkTArray> make_image_renderers() { sk_sp mandrill = GetResourceAsImage("images/mandrill_512.png"); SkTArray> renderers; renderers.push_back(TextureSetRenderer::MakeUnbatched(mandrill)); renderers.push_back(TextureSetRenderer::MakeBatched(mandrill, 0)); renderers.push_back(TextureSetRenderer::MakeBatched(mandrill, kMatrixCount)); renderers.push_back(YUVTextureSetRenderer::MakeFromJPEG( GetResourceAsData("images/mandrill_h1v1.jpg"))); return renderers; } static SkTArray> make_filtered_renderers() { sk_sp mandrill = GetResourceAsImage("images/mandrill_512.png"); SkColorMatrix cm; cm.setSaturation(10); sk_sp colorFilter = SkColorFilters::Matrix(cm); sk_sp imageFilter = SkImageFilters::Dilate(8, 8, nullptr); static constexpr SkColor kAlphas[] = { SK_ColorTRANSPARENT, SK_ColorBLACK }; auto alphaGradient = SkGradientShader::MakeRadial( {0.5f * kTileWidth * kColCount, 0.5f * kTileHeight * kRowCount}, 0.25f * kTileWidth * kColCount, kAlphas, nullptr, 2, SkTileMode::kClamp); sk_sp maskFilter = SkShaderMaskFilter::Make(std::move(alphaGradient)); SkTArray> renderers; renderers.push_back(TextureSetRenderer::MakeAlpha(mandrill, 0.5f)); renderers.push_back(TextureSetRenderer::MakeColorFilter("Saturation", mandrill, std::move(colorFilter))); // NOTE: won't draw correctly until SkCanvas' AutoLoopers are used to handle image filters renderers.push_back(TextureSetRenderer::MakeImageFilter("Dilate", mandrill, std::move(imageFilter))); renderers.push_back(TextureSetRenderer::MakeMaskFilter("Shader", mandrill, std::move(maskFilter))); // NOTE: blur mask filters do work (tested locally), but visually they don't make much // sense, since each quad is blurred independently return renderers; } DEF_GM(return new CompositorGM("debug", make_debug_renderers());) DEF_GM(return new CompositorGM("color", SolidColorRenderer::Make({.2f, .8f, .3f, 1.f}));) DEF_GM(return new CompositorGM("shader", make_shader_renderers());) DEF_GM(return new CompositorGM("image", make_image_renderers());) DEF_GM(return new CompositorGM("filter", make_filtered_renderers());)