| /* | |
| * Copyright 2019 Google LLC | |
| * | |
| * 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/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/SkGradient.h" | |
| #include "include/effects/SkImageFilters.h" | |
| #include "include/effects/SkShaderMaskFilter.h" | |
| #include "include/private/SkTArray.h" | |
| #include "src/core/SkLineClipper.h" | |
| #include "tools/DecodeUtils.h" | |
| #include "tools/Resources.h" | |
| #include "tools/ToolUtils.h" | |
| #include "tools/fonts/FontToolUtils.h" | |
| #include "tools/gpu/YUVUtils.h" | |
| #include <array> | |
| #include <memory> | |
| #include <utility> | |
| using namespace skia_private; | |
| class ClipTileRenderer; | |
| using ClipTileRendererArray = TArray<sk_sp<ClipTileRenderer>>; | |
| // 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, 2}, {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) { | |
| SkFont font(ToolUtils::DefaultPortableTypeface(), 12); | |
| canvas->drawString(text, 0, 0, font, 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: | |
| // 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<SkCanvas::QuadAAFlags>(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 | |
| std::array<SkPoint, 6> points; | |
| 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.copyToQuad(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. | |
| std::array<int, 2> splitIndices; // 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. | |
| STArray<3, std::array<int, 4>> 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]; | |
| std::array<bool, 4> subAA; | |
| int draws = 0; | |
| for (int i = 0; i < subtiles.size(); ++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.data(), | |
| lines + 2, | |
| lineCount - 1, | |
| quadCount); | |
| } | |
| return draws; | |
| } | |
| }; | |
| static constexpr int kMatrixCount = 5; | |
| class CompositorGM : public skiagm::GM { | |
| public: | |
| CompositorGM(const char* name, std::function<ClipTileRendererArray()> makeRendererFn) | |
| : fMakeRendererFn(std::move(makeRendererFn)) | |
| , fName(name) {} | |
| protected: | |
| SkISize getISize() override { | |
| // Initialize the array of renderers. | |
| this->onceBeforeDraw(); | |
| // 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.size() + 75.f)); | |
| } | |
| SkString getName() const override { | |
| SkString fullName; | |
| fullName.appendf("compositor_quads_%s", fName.c_str()); | |
| return fullName; | |
| } | |
| void onOnceBeforeDraw() override { | |
| fRenderers = fMakeRendererFn(); | |
| 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; | |
| TArray<int> drawCounts(fRenderers.size()); | |
| drawCounts.push_back_n(fRenderers.size(), 0); | |
| canvas->save(); | |
| canvas->translate(kOffset + kBannerWidth, kOffset); | |
| for (int i = 0; i < fMatrices.size(); ++i) { | |
| canvas->save(); | |
| draw_text(canvas, fMatrixNames[i].c_str()); | |
| canvas->translate(0.f, kGap); | |
| for (int j = 0; j < fRenderers.size(); ++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.size(); ++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: | |
| std::function<ClipTileRendererArray()> fMakeRendererFn; | |
| ClipTileRendererArray fRenderers; | |
| TArray<SkMatrix> fMatrices; | |
| TArray<SkString> fMatrixNames; | |
| SkString fName; | |
| void configureMatrices() { | |
| fMatrices.clear(); | |
| fMatrixNames.clear(); | |
| 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 | |
| const std::array<SkPoint, 4> src = SkRect::MakeWH(kColCount * kTileWidth, | |
| kRowCount * kTileHeight).toQuad(); | |
| 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)); | |
| fMatrices[4].preTranslate(0.f, 10.f); | |
| fMatrixNames.push_back(SkString("Perspective")); | |
| SkASSERT(fMatrices.size() == fMatrixNames.size()); | |
| } | |
| using INHERITED = skiagm::GM; | |
| }; | |
| //////////////////////////////////////////////////////////////////////////////////////////////// | |
| // Implementations of TileRenderer that color the clipped tiles in various ways | |
| //////////////////////////////////////////////////////////////////////////////////////////////// | |
| class DebugTileRenderer : public ClipTileRenderer { | |
| public: | |
| static sk_sp<ClipTileRenderer> Make() { | |
| // Since aa override is disabled, the quad flags arg doesn't matter. | |
| return sk_sp<ClipTileRenderer>(new DebugTileRenderer(SkCanvas::kAll_QuadAAFlags, false)); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeAA() { | |
| return sk_sp<ClipTileRenderer>(new DebugTileRenderer(SkCanvas::kAll_QuadAAFlags, true)); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeNonAA() { | |
| return sk_sp<ClipTileRenderer>(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; | |
| constexpr 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) {} | |
| using INHERITED = ClipTileRenderer; | |
| }; | |
| // Tests tmp_drawEdgeAAQuad | |
| class SolidColorRenderer : public ClipTileRenderer { | |
| public: | |
| static sk_sp<ClipTileRenderer> Make(const SkColor4f& color) { | |
| return sk_sp<ClipTileRenderer>(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) {} | |
| using INHERITED = ClipTileRenderer; | |
| }; | |
| // Tests drawEdgeAAImageSet(), but can batch the entries together in different ways | |
| class TextureSetRenderer : public ClipTileRenderer { | |
| public: | |
| static sk_sp<ClipTileRenderer> MakeUnbatched(sk_sp<SkImage> image) { | |
| return Make("Texture", "", std::move(image), nullptr, nullptr, nullptr, nullptr, | |
| 1.f, true, 0); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeBatched(sk_sp<SkImage> 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<ClipTileRenderer> MakeShader(const char* name, sk_sp<SkImage> image, | |
| sk_sp<SkShader> shader, bool local) { | |
| return Make("Shader", name, std::move(image), std::move(shader), | |
| nullptr, nullptr, nullptr, 1.f, local, 0); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeColorFilter(const char* name, sk_sp<SkImage> image, | |
| sk_sp<SkColorFilter> filter) { | |
| return Make("Color Filter", name, std::move(image), nullptr, std::move(filter), nullptr, | |
| nullptr, 1.f, false, 0); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeImageFilter(const char* name, sk_sp<SkImage> image, | |
| sk_sp<SkImageFilter> filter) { | |
| return Make("Image Filter", name, std::move(image), nullptr, nullptr, std::move(filter), | |
| nullptr, 1.f, false, 0); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeMaskFilter(const char* name, sk_sp<SkImage> image, | |
| sk_sp<SkMaskFilter> filter) { | |
| return Make("Mask Filter", name, std::move(image), nullptr, nullptr, nullptr, | |
| std::move(filter), 1.f, false, 0); | |
| } | |
| static sk_sp<ClipTileRenderer> MakeAlpha(sk_sp<SkImage> 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<ClipTileRenderer> Make(const char* topBanner, const char* bottomBanner, | |
| sk_sp<SkImage> image, sk_sp<SkShader> shader, | |
| sk_sp<SkColorFilter> colorFilter, | |
| sk_sp<SkImageFilter> imageFilter, | |
| sk_sp<SkMaskFilter> maskFilter, SkScalar paintAlpha, | |
| bool resetAfterEachQuad, int transformCount) { | |
| return sk_sp<ClipTileRenderer>(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.size() == 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.size() - 1]) { | |
| // Add the new matrix | |
| fPreViewMatrices.push_back(preView); | |
| } // else re-use the last matrix | |
| matrixIdx = fPreViewMatrices.size() - 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::RectToRectOrIdentity( | |
| SkRect::MakeWH(kColCount * kTileWidth, kRowCount * kTileHeight), | |
| SkRect::MakeWH(fImage->width(), fImage->height())); | |
| 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<SkImage> fImage; | |
| sk_sp<SkShader> fShader; | |
| sk_sp<SkColorFilter> fColorFilter; | |
| sk_sp<SkImageFilter> fImageFilter; | |
| sk_sp<SkMaskFilter> fMaskFilter; | |
| SkScalar fPaintAlpha; | |
| // Batching rules | |
| bool fResetEachQuad; | |
| int fTransformBatchCount; | |
| TArray<SkPoint> fDstClips; | |
| TArray<SkMatrix> fPreViewMatrices; | |
| TArray<SkCanvas::ImageSetEntry> fSetEntries; | |
| SkMatrix fBaseCTM; | |
| int fBatchCount; | |
| TextureSetRenderer(const char* topBanner, | |
| const char* bottomBanner, | |
| sk_sp<SkImage> image, | |
| sk_sp<SkShader> shader, | |
| sk_sp<SkColorFilter> colorFilter, | |
| sk_sp<SkImageFilter> imageFilter, | |
| sk_sp<SkMaskFilter> 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->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::RectToRectOrIdentity(kTarget, rect); | |
| 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.size() == 0) { | |
| SkASSERT(fDstClips.size() == 0 && fPreViewMatrices.size() == 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.size(); ++i) { | |
| expectedDstClipCount += 4 * fSetEntries[i].fHasClip; | |
| SkASSERT(fSetEntries[i].fMatrixIndex < 0 || | |
| fSetEntries[i].fMatrixIndex < fPreViewMatrices.size()); | |
| } | |
| SkASSERT(expectedDstClipCount == fDstClips.size()); | |
| #endif | |
| SkPaint paint; | |
| SkRect lastTileRect = fSetEntries[fSetEntries.size() - 1].fDstRect; | |
| this->configureTilePaint(lastTileRect, &paint); | |
| canvas->experimental_DrawEdgeAAImageSet( | |
| fSetEntries.begin(), fSetEntries.size(), fDstClips.begin(), | |
| fPreViewMatrices.begin(), SkSamplingOptions(SkFilterMode::kLinear), | |
| &paint, SkCanvas::kFast_SrcRectConstraint); | |
| // Reset for next tile | |
| fDstClips.clear(); | |
| fPreViewMatrices.clear(); | |
| fSetEntries.clear(); | |
| fBatchCount = 0; | |
| return 1; | |
| } | |
| using INHERITED = ClipTileRenderer; | |
| }; | |
| class YUVTextureSetRenderer : public ClipTileRenderer { | |
| public: | |
| static sk_sp<ClipTileRenderer> MakeFromJPEG(sk_sp<SkData> imageData) { | |
| return sk_sp<ClipTileRenderer>(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) { | |
| #if defined(SK_GANESH) | |
| fImage = fYUVData->refImage(canvas->recordingContext(), | |
| sk_gpu_test::LazyYUVImage::Type::kFromPixmaps); | |
| #endif | |
| 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::RectToRectOrIdentity( | |
| SkRect::MakeWH(kColCount * kTileWidth, kRowCount * kTileHeight), | |
| SkRect::MakeWH(fImage->width(), fImage->height())); | |
| 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<sk_gpu_test::LazyYUVImage> fYUVData; | |
| // The last accessed SkImage from fYUVData, held here for easy access by drawTile | |
| sk_sp<SkImage> fImage; | |
| TArray<SkPoint> fDstClips; | |
| TArray<SkCanvas::ImageSetEntry> fSetEntries; | |
| YUVTextureSetRenderer(sk_sp<SkData> 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.size() == 0) { | |
| SkASSERT(fDstClips.size() == 0); | |
| return 0; | |
| } | |
| #ifdef SK_DEBUG | |
| int expectedDstClipCount = 0; | |
| for (int i = 0; i < fSetEntries.size(); ++i) { | |
| expectedDstClipCount += 4 * fSetEntries[i].fHasClip; | |
| } | |
| SkASSERT(expectedDstClipCount == fDstClips.size()); | |
| #endif | |
| SkPaint paint; | |
| paint.setAntiAlias(true); | |
| paint.setBlendMode(SkBlendMode::kSrcOver); | |
| canvas->experimental_DrawEdgeAAImageSet( | |
| fSetEntries.begin(), fSetEntries.size(), fDstClips.begin(), nullptr, | |
| SkSamplingOptions(SkFilterMode::kLinear), &paint, | |
| SkCanvas::kFast_SrcRectConstraint); | |
| // Reset for next tile | |
| fDstClips.clear(); | |
| fSetEntries.clear(); | |
| return 1; | |
| } | |
| using INHERITED = ClipTileRenderer; | |
| }; | |
| static ClipTileRendererArray make_debug_renderers() { | |
| return ClipTileRendererArray{DebugTileRenderer::Make(), | |
| DebugTileRenderer::MakeAA(), | |
| DebugTileRenderer::MakeNonAA()}; | |
| } | |
| static ClipTileRendererArray make_solid_color_renderers() { | |
| return ClipTileRendererArray{SolidColorRenderer::Make({.2f, .8f, .3f, 1.f})}; | |
| } | |
| static ClipTileRendererArray make_shader_renderers() { | |
| static constexpr SkPoint kPts[] = { {0.f, 0.f}, {0.25f * kTileWidth, 0.25f * kTileHeight} }; | |
| static constexpr SkColor4f kColors[] = { SkColors::kBlue, SkColors::kWhite }; | |
| auto gradient = SkShaders::LinearGradient(kPts, {{kColors, {}, SkTileMode::kMirror}, {}}); | |
| auto info = SkImageInfo::Make(1, 1, kAlpha_8_SkColorType, kOpaque_SkAlphaType); | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| bm.eraseColor(SK_ColorWHITE); | |
| sk_sp<SkImage> image = bm.asImage(); | |
| return ClipTileRendererArray{ | |
| TextureSetRenderer::MakeShader("Gradient", image, gradient, false), | |
| TextureSetRenderer::MakeShader("Local Gradient", image, gradient, true)}; | |
| } | |
| static ClipTileRendererArray make_image_renderers() { | |
| sk_sp<SkImage> mandrill = ToolUtils::GetResourceAsImage("images/mandrill_512.png"); | |
| sk_sp<SkData> mandrillJpeg = GetResourceAsData("images/mandrill_h1v1.jpg"); | |
| return ClipTileRendererArray{TextureSetRenderer::MakeUnbatched(mandrill), | |
| TextureSetRenderer::MakeBatched(mandrill, 0), | |
| TextureSetRenderer::MakeBatched(mandrill, kMatrixCount), | |
| YUVTextureSetRenderer::MakeFromJPEG(mandrillJpeg)}; | |
| } | |
| static ClipTileRendererArray make_filtered_renderers() { | |
| sk_sp<SkImage> mandrill = ToolUtils::GetResourceAsImage("images/mandrill_512.png"); | |
| SkColorMatrix cm; | |
| cm.setSaturation(10); | |
| sk_sp<SkColorFilter> colorFilter = SkColorFilters::Matrix(cm); | |
| sk_sp<SkImageFilter> imageFilter = SkImageFilters::Dilate(8, 8, nullptr); | |
| static constexpr SkColor4f kAlphas[] = { SkColors::kTransparent, SkColors::kBlack }; | |
| auto alphaGradient = SkShaders::RadialGradient( | |
| {0.5f * kTileWidth * kColCount, 0.5f * kTileHeight * kRowCount}, | |
| 0.25f * kTileWidth * kColCount, {{kAlphas, {}, SkTileMode::kClamp}, {}}); | |
| sk_sp<SkMaskFilter> maskFilter = SkShaderMaskFilter::Make(std::move(alphaGradient)); | |
| return ClipTileRendererArray{ | |
| TextureSetRenderer::MakeAlpha(mandrill, 0.5f), | |
| TextureSetRenderer::MakeColorFilter("Saturation", mandrill, std::move(colorFilter)), | |
| // NOTE: won't draw correctly until SkCanvas' AutoLoopers are used to handle image filters | |
| TextureSetRenderer::MakeImageFilter("Dilate", mandrill, std::move(imageFilter)), | |
| // NOTE: blur mask filters do work (tested locally), but visually they don't make much | |
| // sense, since each quad is blurred independently | |
| TextureSetRenderer::MakeMaskFilter("Shader", mandrill, std::move(maskFilter))}; | |
| } | |
| DEF_GM(return new CompositorGM("debug", make_debug_renderers);) | |
| DEF_GM(return new CompositorGM("color", make_solid_color_renderers);) | |
| 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);) |