blob: 52c2114a17335b6cf94536147bd364c57e73968f [file] [log] [blame]
/*
* 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>
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
extern int gOverrideMaxTextureSizeGanesh;
extern std::atomic<int> gNumTilesDrawnGanesh;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
extern int gOverrideMaxTextureSizeGraphite;
extern std::atomic<int> gNumTilesDrawnGraphite;
#endif
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
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
gOverrideMaxTextureSizeGanesh = 0;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
gOverrideMaxTextureSizeGraphite = 0;
#endif
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));
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
int actualNumTiles =
gNumTilesDrawnGanesh.load(std::memory_order_acquire);
REPORTER_ASSERT(reporter, actualNumTiles == 0);
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
int actualNumTiles2 =
gNumTilesDrawnGraphite.load(std::memory_order_acquire);
REPORTER_ASSERT(reporter, actualNumTiles2 == 0);
#endif
canvas->restore();
// Then, force 4x4 tiling
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
gOverrideMaxTextureSizeGanesh = kOverrideMaxTextureSize;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
gOverrideMaxTextureSizeGraphite = kOverrideMaxTextureSize;
#endif
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));
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
if (canvas->recordingContext()) {
actualNumTiles =
gNumTilesDrawnGanesh.load(std::memory_order_acquire);
REPORTER_ASSERT(reporter,
numDesiredTiles == actualNumTiles,
"mismatch expected: %d actual: %d\n",
numDesiredTiles,
actualNumTiles);
}
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
if (canvas->recorder()) {
actualNumTiles2 =
gNumTilesDrawnGraphite.load(std::memory_order_acquire);
REPORTER_ASSERT(reporter,
numDesiredTiles == actualNumTiles2,
"mismatch expected: %d actual: %d\n",
numDesiredTiles,
actualNumTiles2);
}
#endif
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);
}
}
}
}
}
}
// Reset tiling behavior
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
gOverrideMaxTextureSizeGanesh = 0;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
gOverrideMaxTextureSizeGraphite = 0;
#endif
}
// 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();
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
gOverrideMaxTextureSizeGanesh = kOverrideMaxTextureSize;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
gOverrideMaxTextureSizeGraphite = kOverrideMaxTextureSize;
#endif
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);
// reset to default behavior
#if defined(SK_GANESH) && defined(GR_TEST_UTILS)
gOverrideMaxTextureSizeGanesh = 0;
#endif
#if defined(SK_GRAPHITE) && defined(GRAPHITE_TEST_UTILS)
gOverrideMaxTextureSizeGraphite = 0;
#endif
}
} // 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