| /* | |
| * Copyright 2023 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkAlphaType.h" | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkColorSpace.h" | |
| #include "include/core/SkColorType.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkMatrix.h" | |
| #include "include/core/SkPaint.h" | |
| #include "include/core/SkPicture.h" | |
| #include "include/core/SkPictureRecorder.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSamplingOptions.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkSurface.h" | |
| #include "include/core/SkTiledImageUtils.h" | |
| #include "include/encode/SkPngEncoder.h" | |
| #include "include/gpu/GpuTypes.h" | |
| #include "include/private/base/SkAssert.h" | |
| #include "src/core/SkSamplingPriv.h" | |
| #include "tests/Test.h" | |
| #include "tests/TestUtils.h" | |
| #include "tools/ToolUtils.h" | |
| #if defined(SK_GANESH) | |
| #include "include/gpu/GrDirectContext.h" | |
| #include "include/gpu/ganesh/SkSurfaceGanesh.h" | |
| #include "src/gpu/ganesh/GrDirectContextPriv.h" | |
| #include "src/gpu/ganesh/GrResourceCache.h" | |
| #include "src/gpu/ganesh/GrSurface.h" | |
| #include "src/gpu/ganesh/GrTexture.h" | |
| #include "tests/CtsEnforcement.h" | |
| struct GrContextOptions; | |
| #endif | |
| #if defined(SK_GRAPHITE) | |
| #include "include/gpu/graphite/Context.h" | |
| #include "include/gpu/graphite/Recorder.h" | |
| #include "include/gpu/graphite/Surface.h" | |
| #include "src/gpu/graphite/Caps.h" | |
| #include "src/gpu/graphite/RecorderPriv.h" | |
| #include "src/gpu/graphite/Texture.h" | |
| #include "tools/GpuToolUtils.h" | |
| #else | |
| namespace skgpu { namespace graphite { class Recorder; } } | |
| #endif | |
| #include <atomic> | |
| #include <functional> | |
| #include <initializer_list> | |
| #include <string.h> | |
| #include <utility> | |
| extern int gOverrideMaxTextureSize; | |
| extern std::atomic<int> gNumTilesDrawn; | |
| namespace { | |
| // Draw a white border around the edge (to test strict constraints) and | |
| // a Hilbert curve inside of that (so the effects of (mis) sampling are evident). | |
| void draw(SkCanvas* canvas, int imgSize, int whiteBandWidth, | |
| int desiredLineWidth, int desiredDepth) { | |
| const int kPad = desiredLineWidth; | |
| canvas->clear(SK_ColorWHITE); | |
| SkPaint innerRect; | |
| innerRect.setColor(SK_ColorDKGRAY); | |
| canvas->drawRect(SkRect::MakeIWH(imgSize, imgSize).makeInset(whiteBandWidth, whiteBandWidth), | |
| innerRect); | |
| int desiredDrawSize = imgSize - 2 * kPad - 2 * whiteBandWidth; | |
| ToolUtils::HilbertGenerator gen(desiredDrawSize, desiredLineWidth, desiredDepth); | |
| canvas->translate(kPad + whiteBandWidth, imgSize - kPad - whiteBandWidth); | |
| gen.draw(canvas); | |
| } | |
| sk_sp<SkImage> make_big_bitmap_image(int imgSize, int whiteBandWidth, | |
| int desiredLineWidth, int desiredDepth) { | |
| SkBitmap bm; | |
| bm.allocN32Pixels(imgSize, imgSize, /* isOpaque= */ true); | |
| SkCanvas canvas(bm); | |
| draw(&canvas, imgSize, whiteBandWidth, desiredLineWidth, desiredDepth); | |
| bm.setImmutable(); | |
| return bm.asImage(); | |
| } | |
| sk_sp<SkImage> make_big_picture_image(int imgSize, int whiteBandWidth, | |
| int desiredLineWidth, int desiredDepth) { | |
| sk_sp<SkPicture> pic; | |
| { | |
| SkPictureRecorder recorder; | |
| SkCanvas* canvas = recorder.beginRecording(SkRect::MakeIWH(imgSize, imgSize)); | |
| draw(canvas, imgSize, whiteBandWidth, desiredLineWidth, desiredDepth); | |
| pic = recorder.finishRecordingAsPicture(); | |
| } | |
| return SkImages::DeferredFromPicture(std::move(pic), | |
| { imgSize, imgSize }, | |
| /* matrix= */ nullptr, | |
| /* paint= */ nullptr, | |
| SkImages::BitDepth::kU8, | |
| SkColorSpace::MakeSRGB()); | |
| } | |
| const char* get_sampling_str(const SkSamplingOptions& sampling) { | |
| if (sampling.isAniso()) { | |
| return "Aniso"; | |
| } else if (sampling.useCubic) { | |
| return "Cubic"; | |
| } else if (sampling.mipmap != SkMipmapMode::kNone) { | |
| return "Mipmap"; | |
| } else if (sampling.filter == SkFilterMode::kLinear) { | |
| return "Linear"; | |
| } else { | |
| return "NN"; | |
| } | |
| } | |
| SkString create_label(GrDirectContext* dContext, | |
| const char* generator, | |
| const SkSamplingOptions& sampling, | |
| int scale, | |
| int rot, | |
| SkCanvas::SrcRectConstraint constraint, | |
| int numTiles) { | |
| SkString label; | |
| label.appendf("%s-%s-%s-%d-%d-%s-%d", | |
| dContext ? "ganesh" : "graphite", | |
| generator, | |
| get_sampling_str(sampling), | |
| scale, | |
| rot, | |
| constraint == SkCanvas::kFast_SrcRectConstraint ? "fast" : "strict", | |
| numTiles); | |
| return label; | |
| } | |
| void potentially_write_to_png(const char* directory, | |
| const SkString& label, | |
| const SkBitmap& bm) { | |
| constexpr bool kWriteOutImages = false; | |
| if constexpr(kWriteOutImages) { | |
| SkString filename; | |
| filename.appendf("//%s//%s.png", directory, label.c_str()); | |
| SkFILEWStream file(filename.c_str()); | |
| SkAssertResult(file.isValid()); | |
| SkAssertResult(SkPngEncoder::Encode(&file, bm.pixmap(), {})); | |
| } | |
| } | |
| bool check_pixels(skiatest::Reporter* reporter, | |
| const SkBitmap& expected, | |
| const SkBitmap& actual, | |
| const SkString& label, | |
| int rot) { | |
| static const float kTols[4] = { 0.008f, 0.008f, 0.008f, 0.008f }; // ~ 2/255 | |
| static const float kRotTols[4] = { 0.024f, 0.024f, 0.024f, 0.024f }; // ~ 6/255 | |
| auto error = std::function<ComparePixmapsErrorReporter>( | |
| [&](int x, int y, const float diffs[4]) { | |
| SkASSERT(x >= 0 && y >= 0); | |
| ERRORF(reporter, "%s: mismatch at %d, %d (%f, %f, %f %f)", | |
| label.c_str(), x, y, diffs[0], diffs[1], diffs[2], diffs[3]); | |
| }); | |
| return ComparePixels(expected.pixmap(), actual.pixmap(), rot ? kRotTols : kTols, error); | |
| } | |
| // Return a clip rect that will result in the number of desired tiles being used. The trick | |
| // is that the clip rect also has to work when rotated. | |
| SkRect clip_rect(SkRect dstRect, int numDesiredTiles) { | |
| dstRect.outset(5, 5); | |
| switch (numDesiredTiles) { | |
| case 0: | |
| return { dstRect.fLeft-64, dstRect.fTop-64, dstRect.fLeft-63, dstRect.fTop-63 }; | |
| case 4: { | |
| // Upper left 4x4 | |
| float outset = 0.125f * dstRect.width() * SK_ScalarRoot2Over2; | |
| SkPoint center = dstRect.center(); | |
| return { center.fX - outset, center.fY - outset, | |
| center.fX + outset, center.fY + outset }; | |
| } | |
| case 9: { | |
| // Upper left 3x3 | |
| float outset = 0.25f * dstRect.width() * SK_ScalarRoot2Over2; | |
| SkPoint center = dstRect.center(); | |
| center.offset(-dstRect.width()/8.0f, -dstRect.height()/8.0f); | |
| return { center.fX - outset, center.fY - outset, | |
| center.fX + outset, center.fY + outset }; | |
| } | |
| } | |
| return dstRect; // all 16 tiles | |
| } | |
| bool difficult_case(const SkSamplingOptions& sampling, | |
| int scale, | |
| int rot, | |
| SkCanvas::SrcRectConstraint constraint) { | |
| if (sampling.useCubic) { | |
| return false; // cubic never causes any issues | |
| } | |
| if (constraint == SkCanvas::kStrict_SrcRectConstraint && | |
| (sampling.mipmap != SkMipmapMode::kNone || sampling.filter == SkFilterMode::kLinear)) { | |
| // linear-filtered strict big image drawing is currently broken (b/286239467). The issue | |
| // is that the strict constraint is propagated to the child tiles which breaks the | |
| // interpolation expected in the middle of the large image. | |
| // Note that strict mipmapping is auto-downgraded to strict linear sampling. | |
| return true; | |
| } | |
| if (sampling.mipmap == SkMipmapMode::kLinear) { | |
| // Mipmapping is broken for anything other that 1-to-1 draws (b/286256104). The issue | |
| // is that the mipmaps are created for each tile individually so the higher levels differ | |
| // from what would be generated with the entire image. Mipmapped draws are off by ~20/255 | |
| // at 4x and ~64/255 at 8x) | |
| return scale > 1; | |
| } | |
| if (sampling.filter == SkFilterMode::kNearest) { | |
| // Perhaps unsurprisingly, NN only passes on un-rotated 1-to-1 draws (off by ~187/255 at | |
| // different scales). | |
| return scale > 1 || rot > 0; | |
| } | |
| return false; | |
| } | |
| // compare tiled and untiled draws - varying the parameters (e.g., sampling, rotation, fast vs. | |
| // strict, etc). | |
| void tiling_comparison_test(GrDirectContext* dContext, | |
| skgpu::graphite::Recorder* recorder, | |
| skiatest::Reporter* reporter) { | |
| // We're using the knowledge that the internal tile size is 1024. By creating kImageSize | |
| // sized images we know we'll get a 4x4 tiling regardless of the sampling. | |
| static const int kImageSize = 4096 - 4 * 2 * kBicubicFilterTexelPad; | |
| static const int kOverrideMaxTextureSize = 1024; | |
| #if defined(SK_GANESH) | |
| if (dContext && dContext->maxTextureSize() < kImageSize) { | |
| // For the expected images we need to be able to draw w/o tiling | |
| return; | |
| } | |
| #endif | |
| #if defined(SK_GRAPHITE) | |
| if (recorder) { | |
| const skgpu::graphite::Caps* caps = recorder->priv().caps(); | |
| if (caps->maxTextureSize() < kImageSize) { | |
| return; | |
| } | |
| } | |
| #endif | |
| static const int kWhiteBandWidth = 4; | |
| const SkRect srcRect = SkRect::MakeIWH(kImageSize, kImageSize).makeInset(kWhiteBandWidth, | |
| kWhiteBandWidth); | |
| using GeneratorT = sk_sp<SkImage>(*)(int imgSize, int whiteBandWidth, | |
| int desiredLineWidth, int desiredDepth); | |
| static const struct { | |
| GeneratorT fGen; | |
| const char* fTag; | |
| } kGenerators[] = { { make_big_bitmap_image, "BM" }, | |
| { make_big_picture_image, "Picture" } }; | |
| static const SkSamplingOptions kSamplingOptions[] = { | |
| SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNone), | |
| SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone), | |
| // Note that Mipmapping gets auto-disabled with a strict-constraint | |
| SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear), | |
| SkSamplingOptions(SkCubicResampler::CatmullRom()), | |
| }; | |
| int numClippedTiles = 9; | |
| for (auto gen : kGenerators) { | |
| if (recorder && !strcmp(gen.fTag, "Picture")) { | |
| // In the picture-image case, the non-tiled code path draws the picture directly into a | |
| // gpu-backed surface while the tiled code path the picture is draws the picture into | |
| // a raster-backed surface. For Ganesh this works out, since both Ganesh and Raster | |
| // support non-AA rect draws. For Graphite the results are very different (since | |
| // Graphite always anti-aliases. Forcing all the rect draws to be AA doesn't work out | |
| // since AA introduces too much variance between both of the gpu backends and Raster - | |
| // which would obscure any errors introduced by tiling. | |
| continue; | |
| } | |
| sk_sp<SkImage> img = (*gen.fGen)(kImageSize, | |
| kWhiteBandWidth, | |
| /* desiredLineWidth= */ 16, | |
| /* desiredDepth= */ 7); | |
| numClippedTiles = (numClippedTiles == 9) ? 4 : 9; // alternate to reduce the combinatorics | |
| for (int scale : { 1, 4, 8 }) { | |
| for (int rot : { 0, 45 }) { | |
| for (int numDesiredTiles : { numClippedTiles, 16 }) { | |
| SkRect destRect = SkRect::MakeWH(srcRect.width()/scale, | |
| srcRect.height()/scale); | |
| SkMatrix m = SkMatrix::RotateDeg(rot, destRect.center()); | |
| SkIRect rotatedRect = m.mapRect(destRect).roundOut(); | |
| rotatedRect.outset(2, 2); // outset to capture the constraint's effect | |
| SkRect clipRect = clip_rect(destRect, numDesiredTiles); | |
| auto destII = SkImageInfo::Make(rotatedRect.width(), | |
| rotatedRect.height(), | |
| kRGBA_8888_SkColorType, | |
| kPremul_SkAlphaType); | |
| SkBitmap expected, actual; | |
| expected.allocPixels(destII); | |
| actual.allocPixels(destII); | |
| sk_sp<SkSurface> surface; | |
| #if defined(SK_GANESH) | |
| if (dContext) { | |
| surface = SkSurfaces::RenderTarget(dContext, | |
| skgpu::Budgeted::kNo, | |
| destII); | |
| } | |
| #endif | |
| #if defined(SK_GRAPHITE) | |
| if (recorder) { | |
| surface = SkSurfaces::RenderTarget(recorder, destII); | |
| } | |
| #endif | |
| for (auto sampling : kSamplingOptions) { | |
| for (auto constraint : { SkCanvas::kStrict_SrcRectConstraint, | |
| SkCanvas::kFast_SrcRectConstraint }) { | |
| if (difficult_case(sampling, scale, rot, constraint)) { | |
| continue; | |
| } | |
| SkString label = create_label(dContext, gen.fTag, sampling, scale, rot, | |
| constraint, numDesiredTiles); | |
| SkCanvas* canvas = surface->getCanvas(); | |
| SkAutoCanvasRestore acr(canvas, /* doSave= */ true); | |
| canvas->translate(-rotatedRect.fLeft, -rotatedRect.fTop); | |
| if (sampling.useCubic || sampling.filter != SkFilterMode::kNearest) { | |
| // NN sampling doesn't deal well w/ the (0.5, 0.5) offset but the | |
| // other sampling modes need it to exercise strict vs. fast | |
| // constraint in non-rotated draws | |
| canvas->translate(0.5f, 0.5f); | |
| } | |
| canvas->concat(m); | |
| // First, draw w/o tiling | |
| gOverrideMaxTextureSize = 0; | |
| gNumTilesDrawn.store(0, std::memory_order_relaxed); | |
| canvas->clear(SK_ColorBLACK); | |
| canvas->save(); | |
| canvas->clipRect(clipRect); | |
| SkTiledImageUtils::DrawImageRect(canvas, img, srcRect, destRect, | |
| sampling, /* paint= */ nullptr, | |
| constraint); | |
| SkAssertResult(surface->readPixels(expected, 0, 0)); | |
| int actualNumTiles = gNumTilesDrawn.load(std::memory_order_acquire); | |
| REPORTER_ASSERT(reporter, actualNumTiles == 0); | |
| canvas->restore(); | |
| // Then, force 4x4 tiling | |
| gOverrideMaxTextureSize = kOverrideMaxTextureSize; | |
| canvas->clear(SK_ColorBLACK); | |
| canvas->save(); | |
| canvas->clipRect(clipRect); | |
| SkTiledImageUtils::DrawImageRect(canvas, img, srcRect, destRect, | |
| sampling, /* paint= */ nullptr, | |
| constraint); | |
| SkAssertResult(surface->readPixels(actual, 0, 0)); | |
| actualNumTiles = gNumTilesDrawn.load(std::memory_order_acquire); | |
| REPORTER_ASSERT(reporter, numDesiredTiles == actualNumTiles, | |
| "mismatch expected: %d actual: %d\n", | |
| numDesiredTiles, actualNumTiles); | |
| canvas->restore(); | |
| REPORTER_ASSERT(reporter, check_pixels(reporter, expected, actual, | |
| label, rot)); | |
| potentially_write_to_png("expected", label, expected); | |
| potentially_write_to_png("actual", label, actual); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // In this test we draw the same bitmap-backed image twice and check that we only upload it once. | |
| // Everything is set up for the bitmap-backed image to be split into 16 1024x1024 tiles. | |
| void tiled_image_caching_test(GrDirectContext* dContext, | |
| skgpu::graphite::Recorder* recorder, | |
| skiatest::Reporter* reporter) { | |
| static const int kImageSize = 4096; | |
| static const int kOverrideMaxTextureSize = 1024; | |
| static const SkISize kExpectedTileSize { kOverrideMaxTextureSize, kOverrideMaxTextureSize }; | |
| sk_sp<SkImage> img = make_big_bitmap_image(kImageSize, | |
| /* whiteBandWidth= */ 0, | |
| /* desiredLineWidth= */ 16, | |
| /* desiredDepth= */ 7); | |
| auto destII = SkImageInfo::Make(kImageSize, kImageSize, | |
| kRGBA_8888_SkColorType, | |
| kPremul_SkAlphaType); | |
| SkBitmap readback; | |
| readback.allocPixels(destII); | |
| sk_sp<SkSurface> surface; | |
| #if defined(SK_GANESH) | |
| if (dContext) { | |
| surface = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, destII); | |
| } | |
| #endif | |
| #if defined(SK_GRAPHITE) | |
| if (recorder) { | |
| surface = SkSurfaces::RenderTarget(recorder, destII); | |
| } | |
| #endif | |
| if (!surface) { | |
| return; | |
| } | |
| SkCanvas* canvas = surface->getCanvas(); | |
| gOverrideMaxTextureSize = kOverrideMaxTextureSize; | |
| for (int i = 0; i < 2; ++i) { | |
| canvas->clear(SK_ColorBLACK); | |
| SkTiledImageUtils::DrawImage(canvas, img, | |
| /* x= */ 0, /* y= */ 0, | |
| SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNone), | |
| /* paint= */ nullptr, | |
| SkCanvas::kFast_SrcRectConstraint); | |
| SkAssertResult(surface->readPixels(readback, 0, 0)); | |
| } | |
| int numFound = 0; | |
| #if defined(SK_GANESH) | |
| if (dContext) { | |
| GrResourceCache* cache = dContext->priv().getResourceCache(); | |
| cache->visitSurfaces([&](const GrSurface* surf, bool /* purgeable */) { | |
| const GrTexture* tex = surf->asTexture(); | |
| if (tex && tex->dimensions() == kExpectedTileSize) { | |
| ++numFound; | |
| } | |
| }); | |
| } | |
| #endif | |
| #if defined(SK_GRAPHITE) | |
| if (recorder) { | |
| skgpu::graphite::ResourceCache* cache = recorder->priv().resourceCache(); | |
| cache->visitTextures([&](const skgpu::graphite::Texture* tex, bool /* purgeable */) { | |
| if (tex->dimensions() == kExpectedTileSize) { | |
| ++numFound; | |
| } | |
| }); | |
| } | |
| #endif | |
| REPORTER_ASSERT(reporter, numFound == 16, "Expected: 16 Actual: %d", numFound); | |
| gOverrideMaxTextureSize = 0; | |
| } | |
| } // anonymous namespace | |
| #if defined(SK_GANESH) | |
| // TODO(b/306005622): fix in SkQP and move to CtsEnforcement::kNextRelease | |
| DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(BigImageTest_Ganesh, | |
| reporter, | |
| ctxInfo, | |
| CtsEnforcement::kNever) { | |
| auto dContext = ctxInfo.directContext(); | |
| tiling_comparison_test(dContext, /* recorder= */ nullptr, reporter); | |
| } | |
| // TODO(b/306005622): fix in SkQP and move to CtsEnforcement::kNextRelease | |
| DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(TiledDrawCacheTest_Ganesh, | |
| reporter, | |
| ctxInfo, | |
| CtsEnforcement::kNever) { | |
| auto dContext = ctxInfo.directContext(); | |
| tiled_image_caching_test(dContext, /* recorder= */ nullptr, reporter); | |
| } | |
| #endif // SK_GANESH | |
| #if defined(SK_GRAPHITE) | |
| // TODO(b/306005622): fix in SkQP and move to CtsEnforcement::kNextRelease | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(BigImageTest_Graphite, | |
| reporter, | |
| context, | |
| CtsEnforcement::kNever) { | |
| std::unique_ptr<skgpu::graphite::Recorder> recorder = | |
| context->makeRecorder(ToolUtils::CreateTestingRecorderOptions()); | |
| tiling_comparison_test(/* dContext= */ nullptr, recorder.get(), reporter); | |
| } | |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(TiledDrawCacheTest_Graphite, | |
| reporter, | |
| context, | |
| CtsEnforcement::kNextRelease) { | |
| std::unique_ptr<skgpu::graphite::Recorder> recorder = | |
| context->makeRecorder(ToolUtils::CreateTestingRecorderOptions()); | |
| tiled_image_caching_test(/* dContext= */ nullptr, recorder.get(), reporter); | |
| } | |
| #endif // SK_GRAPHITE |