/* * 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/SkBitmap.h" #include "include/core/SkBlendMode.h" #include "include/core/SkCanvas.h" #include "include/core/SkClipOp.h" #include "include/core/SkColor.h" #include "include/core/SkColorFilter.h" #include "include/core/SkColorSpace.h" #include "include/core/SkColorType.h" #include "include/core/SkData.h" #include "include/core/SkImage.h" #include "include/core/SkImageInfo.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/SkSamplingOptions.h" #include "include/core/SkScalar.h" #include "include/core/SkSize.h" #include "include/core/SkString.h" #include "include/core/SkTileMode.h" #include "include/private/base/SkAssert.h" #include "include/private/base/SkDebug.h" #include "include/private/base/SkTArray.h" #include "include/private/base/SkTo.h" #include "src/core/SkColorData.h" #include "src/core/SkDevice.h" #include "src/core/SkImageFilterTypes.h" #include "src/core/SkMatrixPriv.h" #include "src/core/SkRectPriv.h" #include "src/core/SkSpecialImage.h" #include "src/effects/colorfilters/SkColorFilterBase.h" #include "src/gpu/ganesh/image/GrImageUtils.h" #include "tests/CtsEnforcement.h" #include "tests/Test.h" #include "tools/EncodeUtils.h" #include "tools/gpu/ContextType.h" #include #include #include #include #include #include #include #if defined(SK_GRAPHITE) #include "include/gpu/graphite/Context.h" #include "src/gpu/graphite/ContextPriv.h" #include "src/gpu/graphite/RecorderPriv.h" #include "src/gpu/graphite/SpecialImage_Graphite.h" #include "src/gpu/graphite/TextureProxyView.h" #include "src/gpu/graphite/TextureUtils.h" #endif #if defined(SK_GANESH) #include "include/gpu/ganesh/GrDirectContext.h" #include "include/gpu/ganesh/GrRecordingContext.h" #include "include/gpu/ganesh/GrTypes.h" struct GrContextOptions; #endif class SkShader; using namespace skia_private; using namespace skif; // NOTE: Not in anonymous so that FilterResult can friend it class FilterResultTestAccess { using BoundsAnalysis = FilterResult::BoundsAnalysis; public: static void Draw(const skif::Context& ctx, SkDevice* device, const skif::FilterResult& image, bool preserveDeviceState) { image.draw(ctx, device, preserveDeviceState, /*blender=*/nullptr); } static sk_sp AsShader(const skif::Context& ctx, const skif::FilterResult& image, const skif::LayerSpace& sampleBounds) { return image.asShader(ctx, FilterResult::kDefaultSampling, FilterResult::ShaderFlags::kNone, sampleBounds); } static sk_sp StrictShader(const skif::Context& ctx, const skif::FilterResult& image) { auto analysis = image.analyzeBounds(ctx.desiredOutput()); if (analysis & FilterResult::BoundsAnalysis::kRequiresLayerCrop) { // getAnalyzedShaderView() doesn't include the layer crop, this will be handled by // the FilterResultImageResolver. return nullptr; } else { // Add flags to ensure no deferred effects or clamping logic are optimized away. analysis |= BoundsAnalysis::kDstBoundsNotCovered; analysis |= BoundsAnalysis::kRequiresShaderTiling; if (image.tileMode() == SkTileMode::kDecal) { analysis |= BoundsAnalysis::kRequiresDecalInLayerSpace; } return image.getAnalyzedShaderView(ctx, image.sampling(), analysis); } } static skif::FilterResult Rescale(const skif::Context& ctx, const skif::FilterResult& image, const skif::LayerSpace scale) { return image.rescale(ctx, scale, /*enforceDecal=*/false); } static void TrackStats(skif::Context* ctx, skif::Stats* stats) { ctx->fStats = stats; } static bool IsIntegerTransform(const skif::FilterResult& image) { SkMatrix m = SkMatrix(image.fTransform); return m.isTranslate() && SkScalarIsInt(m.getTranslateX()) && SkScalarIsInt(m.getTranslateY()); } static std::optional> DeferredScaleFactors( const skif::FilterResult& image) { float scaleFactors[2]; if (SkMatrix(image.fTransform).getMinMaxScales(scaleFactors)) { return {{scaleFactors[0], scaleFactors[1]}}; } else { return {}; } } enum class ShaderSampleMode { kFast, kShaderClamp, kShaderTile }; static ShaderSampleMode GetExpectedShaderSampleMode(const skif::Context& ctx, const skif::FilterResult& image, bool actionSupportsDirectDrawing) { if (!image) { return ShaderSampleMode::kFast; } auto analysis = image.analyzeBounds(ctx.desiredOutput()); bool mustFillDecal = image.tileMode() == SkTileMode::kDecal && (analysis & BoundsAnalysis::kDstBoundsNotCovered) && !actionSupportsDirectDrawing; if ((analysis & BoundsAnalysis::kHasLayerFillingEffect) || mustFillDecal) { // The image won't be drawn directly so some form of shader is needed. The faster clamp // can be used when clamping explicitly or decal-with-transparent-padding. if (image.tileMode() == SkTileMode::kClamp || (image.tileMode() == SkTileMode::kDecal && image.fBoundary == FilterResult::PixelBoundary::kTransparent)) { return ShaderSampleMode::kShaderClamp; } else { // These cases should be covered by the more expensive shader tiling. return ShaderSampleMode::kShaderTile; } } // If we got here, it will be drawn directly but a clamp can be needed if the data outside // the image is unknown and sampling might pull those values in accidentally. if (image.fBoundary == FilterResult::PixelBoundary::kUnknown) { return ShaderSampleMode::kShaderClamp; } else { return ShaderSampleMode::kFast; } } }; namespace { // Parameters controlling the fuzziness matching of expected and actual images. // NOTE: When image fuzzy diffing fails it will print the expected image, the actual image, and // an "error" image where all bad pixels have been set to red. You can select all three base64 // encoded PNGs, copy them, and run the following command to view in detail: // xsel -o | viewer --file stdin static constexpr float kRGBTolerance = 8.f / 255.f; static constexpr float kAATolerance = 2.f / 255.f; static constexpr float kDefaultMaxAllowedPercentImageDiff = 1.f; static const float kFuzzyKernel[3][3] = {{0.9f, 0.9f, 0.9f}, {0.9f, 1.0f, 0.9f}, {0.9f, 0.9f, 0.9f}}; static_assert(std::size(kFuzzyKernel) == std::size(kFuzzyKernel[0]), "Kernel must be square"); static constexpr int kKernelSize = std::size(kFuzzyKernel); static constexpr bool kLogAllBitmaps = false; // Spammy, recommend limiting test cases being run bool colorfilter_equals(const SkColorFilter* actual, const SkColorFilter* expected) { if (!actual || !expected) { return !actual && !expected; // both null } // The two filter objects are equal if they serialize to the same structure sk_sp actualData = actual->serialize(); sk_sp expectedData = expected->serialize(); return actualData && actualData->equals(expectedData.get()); } void clear_device(SkDevice* device) { SkPaint p; p.setColor4f(SkColors::kTransparent, /*colorSpace=*/nullptr); p.setBlendMode(SkBlendMode::kSrc); device->drawPaint(p); } static constexpr SkTileMode kTileModes[4] = {SkTileMode::kClamp, SkTileMode::kRepeat, SkTileMode::kMirror, SkTileMode::kDecal}; enum class Expect { kDeferredImage, // i.e. modified properties of FilterResult instead of rendering kNewImage, // i.e. rendered a new image before modifying other properties kEmptyImage, // i.e. everything is transparent black }; class ApplyAction { struct TransformParams { LayerSpace fMatrix; SkSamplingOptions fSampling; }; struct CropParams { LayerSpace fRect; SkTileMode fTileMode; // Sometimes the expected bounds due to cropping and tiling are too hard to automate with // simple test code. std::optional> fExpectedBounds; }; struct RescaleParams { LayerSpace fScale; }; public: ApplyAction(const SkMatrix& transform, const SkSamplingOptions& sampling, Expect expectation, const SkSamplingOptions& expectedSampling, SkTileMode expectedTileMode, sk_sp expectedColorFilter) : fAction{TransformParams{LayerSpace(transform), sampling}} , fExpectation(expectation) , fExpectedSampling(expectedSampling) , fExpectedTileMode(expectedTileMode) , fExpectedColorFilter(std::move(expectedColorFilter)) {} ApplyAction(const SkIRect& cropRect, SkTileMode tileMode, std::optional> expectedBounds, Expect expectation, const SkSamplingOptions& expectedSampling, SkTileMode expectedTileMode, sk_sp expectedColorFilter) : fAction{CropParams{LayerSpace(cropRect), tileMode, expectedBounds}} , fExpectation(expectation) , fExpectedSampling(expectedSampling) , fExpectedTileMode(expectedTileMode) , fExpectedColorFilter(std::move(expectedColorFilter)) {} ApplyAction(sk_sp colorFilter, Expect expectation, const SkSamplingOptions& expectedSampling, SkTileMode expectedTileMode, sk_sp expectedColorFilter) : fAction(std::move(colorFilter)) , fExpectation(expectation) , fExpectedSampling(expectedSampling) , fExpectedTileMode(expectedTileMode) , fExpectedColorFilter(std::move(expectedColorFilter)) {} ApplyAction(LayerSpace scale, Expect expectation, const SkSamplingOptions& expectedSampling, SkTileMode expectedTileMode, sk_sp expectedColorFilter) : fAction(RescaleParams{scale}) , fExpectation(expectation) , fExpectedSampling(expectedSampling) , fExpectedTileMode(expectedTileMode) , fExpectedColorFilter(std::move(expectedColorFilter)) {} // Test-simplified logic for bounds propagation similar to how image filters calculate bounds // while evaluating a filter DAG, which is outside of skif::FilterResult's responsibilities. LayerSpace requiredInput(const LayerSpace& desiredOutput) const { if (auto* t = std::get_if(&fAction)) { LayerSpace out; return t->fMatrix.inverseMapRect(desiredOutput, &out) ? out : LayerSpace::Empty(); } else if (auto* c = std::get_if(&fAction)) { LayerSpace intersection = c->fRect; if (c->fTileMode == SkTileMode::kDecal && !intersection.intersect(desiredOutput)) { intersection = LayerSpace::Empty(); } return intersection; } else if (std::holds_alternative>(fAction) || std::holds_alternative(fAction)) { return desiredOutput; } SkUNREACHABLE; } // Performs the action to be tested FilterResult apply(const Context& ctx, const FilterResult& in) const { if (auto* t = std::get_if(&fAction)) { return in.applyTransform(ctx, t->fMatrix, t->fSampling); } else if (auto* c = std::get_if(&fAction)) { return in.applyCrop(ctx, c->fRect, c->fTileMode); } else if (auto* cf = std::get_if>(&fAction)) { return in.applyColorFilter(ctx, *cf); } else if (auto* s = std::get_if(&fAction)) { return FilterResultTestAccess::Rescale(ctx, in, s->fScale); } SkUNREACHABLE; } Expect expectation() const { return fExpectation; } const SkSamplingOptions& expectedSampling() const { return fExpectedSampling; } SkTileMode expectedTileMode() const { return fExpectedTileMode; } const SkColorFilter* expectedColorFilter() const { return fExpectedColorFilter.get(); } std::vector expectedOffscreenSurfaces(const FilterResult& source) const { if (fExpectation != Expect::kNewImage) { return {0}; } if (auto* s = std::get_if(&fAction)) { float minScale = std::min(s->fScale.width(), s->fScale.height()); if (minScale >= 1.f - 0.001f) { return {1}; } else { auto deferredScale = FilterResultTestAccess::DeferredScaleFactors(source); int steps = 0; if (deferredScale && std::get<0>(*deferredScale) <= 0.9f) { steps++; } do { steps++; minScale *= 2.f; } while(minScale < 0.9f); // Rescaling periodic tiling may require scaling further than the value stored in // the action to hit pixel integer bounds, which may trigger one more pass. SkTileMode srcTileMode = source.tileMode(); if (srcTileMode == SkTileMode::kRepeat || srcTileMode == SkTileMode::kMirror) { return {steps, steps + 1}; } else { return {steps}; } } } else { return {1}; } } FilterResultTestAccess::ShaderSampleMode expectedSampleMode(const Context& ctx, const FilterResult& source) const { bool actionSupportsDirectDrawing = true; if (std::holds_alternative(fAction)) { // rescale() normally does not draw directly; the exception is if the source image has // a scale factor that requires a pre-resolve. If that happens 'source' is not really // the source of the rescale steps, and `source` can be drawn directly by the resolve. auto scales = FilterResultTestAccess::DeferredScaleFactors(source); if (!scales || scales->first > 0.5f) { actionSupportsDirectDrawing = false; // no pre-resolve } } return FilterResultTestAccess::GetExpectedShaderSampleMode( ctx, source, actionSupportsDirectDrawing); } LayerSpace expectedBounds(const LayerSpace& inputBounds) const { // This assumes anything outside 'inputBounds' is transparent black. if (auto* t = std::get_if(&fAction)) { if (inputBounds.isEmpty()) { return LayerSpace::Empty(); } return t->fMatrix.mapRect(inputBounds); } else if (auto* c = std::get_if(&fAction)) { if (c->fExpectedBounds) { return *c->fExpectedBounds; } LayerSpace intersection = c->fRect; if (!intersection.intersect(inputBounds)) { return LayerSpace::Empty(); } return c->fTileMode == SkTileMode::kDecal ? intersection : LayerSpace(SkRectPriv::MakeILarge()); } else if (auto* cf = std::get_if>(&fAction)) { if (as_CFB(*cf)->affectsTransparentBlack()) { // Fills out infinitely return LayerSpace(SkRectPriv::MakeILarge()); } else { return inputBounds; } } else if (std::holds_alternative(fAction)) { return inputBounds; } SkUNREACHABLE; } sk_sp renderExpectedImage(const Context& ctx, sk_sp source, LayerSpace origin, const LayerSpace& desiredOutput) const { Expect effectiveExpectation = fExpectation; SkISize size(desiredOutput.size()); if (desiredOutput.isEmpty()) { size = {1, 1}; effectiveExpectation = Expect::kEmptyImage; } auto device = ctx.backend()->makeDevice(size, ctx.refColorSpace()); if (!device) { return nullptr; } SkCanvas canvas{device}; canvas.clear(SK_ColorTRANSPARENT); canvas.translate(-desiredOutput.left(), -desiredOutput.top()); if (effectiveExpectation != Expect::kEmptyImage) { SkASSERT(source); LayerSpace sourceBounds{ SkIRect::MakeXYWH(origin.x(), origin.y(), source->width(), source->height())}; LayerSpace expectedBounds = this->expectedBounds(sourceBounds); canvas.clipIRect(SkIRect(expectedBounds), SkClipOp::kIntersect); SkPaint paint; paint.setAntiAlias(true); paint.setBlendMode(SkBlendMode::kSrc); // Start with NN to match exact subsetting FilterResult does for deferred images SkSamplingOptions sampling = {}; SkTileMode tileMode = SkTileMode::kDecal; if (auto* t = std::get_if(&fAction)) { SkMatrix m{t->fMatrix}; // FilterResult treats default/bilerp filtering as NN when it has an integer // translation, so only change 'sampling' when that is not the case. if (!m.isTranslate() || !SkScalarIsInt(m.getTranslateX()) || !SkScalarIsInt(m.getTranslateY())) { sampling = t->fSampling; } canvas.concat(m); } else if (auto* c = std::get_if(&fAction)) { LayerSpace imageBounds( SkIRect::MakeXYWH(origin.x(), origin.y(), source->width(), source->height())); if (c->fTileMode == SkTileMode::kDecal || imageBounds.contains(c->fRect)) { // Extract a subset of the image SkAssertResult(imageBounds.intersect(c->fRect)); source = source->makeSubset({imageBounds.left() - origin.x(), imageBounds.top() - origin.y(), imageBounds.right() - origin.x(), imageBounds.bottom() - origin.y()}); origin = imageBounds.topLeft(); } else { // A non-decal tile mode where the image doesn't cover the crop requires the // image to be padded out with transparency so the tiling matches 'fRect'. SkISize paddedSize = SkISize(c->fRect.size()); auto paddedDevice = ctx.backend()->makeDevice(paddedSize, ctx.refColorSpace()); clear_device(paddedDevice.get()); paddedDevice->drawSpecial(source.get(), SkMatrix::Translate(origin.x() - c->fRect.left(), origin.y() - c->fRect.top()), /*sampling=*/{}, /*paint=*/{}); source = paddedDevice->snapSpecial(SkIRect::MakeSize(paddedSize)); origin = c->fRect.topLeft(); } tileMode = c->fTileMode; } else if (auto* cf = std::get_if>(&fAction)) { paint.setColorFilter(*cf); } else if (auto* s = std::get_if(&fAction)) { // Don't redraw with an identity scale since sampling errors creep in on some GPUs if (s->fScale.width() != 1.f || s->fScale.height() != 1.f) { int origSrcWidth = source->width(); int origSrcHeight = source->height(); SkISize lowResSize = {sk_float_ceil2int(origSrcWidth * s->fScale.width()), sk_float_ceil2int(origSrcHeight * s->fScale.height())}; while (source->width() != lowResSize.width() || source->height() != lowResSize.height()) { float sx = std::max(0.5f, lowResSize.width() / (float) source->width()); float sy = std::max(0.5f, lowResSize.height() / (float) source->height()); SkISize stepSize = {sk_float_ceil2int(source->width() * sx), sk_float_ceil2int(source->height() * sy)}; auto stepDevice = ctx.backend()->makeDevice(stepSize, ctx.refColorSpace()); clear_device(stepDevice.get()); stepDevice->drawSpecial(source.get(), SkMatrix::Scale(sx, sy), SkFilterMode::kLinear, /*paint=*/{}); source = stepDevice->snapSpecial(SkIRect::MakeSize(stepSize)); } // Adjust to draw the low-res image upscaled to fill the original image bounds sampling = SkFilterMode::kLinear; tileMode = SkTileMode::kClamp; canvas.translate(origin.x(), origin.y()); canvas.scale(origSrcWidth / (float) source->width(), origSrcHeight / (float) source->height()); origin = LayerSpace({0, 0}); } } // else it's a rescale action, but for the expected image leave it unmodified. paint.setShader(source->asShader(tileMode, sampling, SkMatrix::Translate(origin.x(), origin.y()))); canvas.drawPaint(paint); } return device->snapSpecial(SkIRect::MakeSize(size)); } private: // Action std::variant,// for applyColorFilter() RescaleParams // for rescale() > fAction; // Expectation Expect fExpectation; SkSamplingOptions fExpectedSampling; SkTileMode fExpectedTileMode; sk_sp fExpectedColorFilter; // The expected desired outputs and layer bounds are calculated automatically based on the // action type and parameters to simplify test case specification. }; class FilterResultImageResolver { public: enum class Method { kImageAndOffset, kDrawToCanvas, kShader, kClippedShader, kStrictShader // Only used to check image correctness when stats reported an optimization }; FilterResultImageResolver(Method method) : fMethod(method) {} const char* methodName() const { switch (fMethod) { case Method::kImageAndOffset: return "imageAndOffset"; case Method::kDrawToCanvas: return "drawToCanvas"; case Method::kShader: return "asShader"; case Method::kClippedShader: return "asShaderClipped"; case Method::kStrictShader: return "strictShader"; } SkUNREACHABLE; } std::pair, SkIPoint> resolve(const Context& ctx, const FilterResult& image) const { if (fMethod == Method::kImageAndOffset) { SkIPoint origin; sk_sp resolved = image.imageAndOffset(ctx, &origin); return {resolved, origin}; } else { if (ctx.desiredOutput().isEmpty()) { return {nullptr, {}}; } auto device = ctx.backend()->makeDevice(SkISize(ctx.desiredOutput().size()), ctx.refColorSpace()); SkASSERT(device); SkCanvas canvas{device}; canvas.clear(SK_ColorTRANSPARENT); canvas.translate(-ctx.desiredOutput().left(), -ctx.desiredOutput().top()); if (fMethod > Method::kDrawToCanvas) { sk_sp shader; if (fMethod == Method::kShader) { // asShader() applies layer bounds by resolving automatically // (e.g. kDrawToCanvas), if sampleBounds is larger than the layer bounds. Since // we want to test the unclipped shader version, pass in layerBounds() for // sampleBounds and add a clip to the canvas instead. canvas.clipIRect(SkIRect(image.layerBounds())); shader = FilterResultTestAccess::AsShader(ctx, image, image.layerBounds()); } else if (fMethod == Method::kClippedShader) { shader = FilterResultTestAccess::AsShader(ctx, image, ctx.desiredOutput()); } else { shader = FilterResultTestAccess::StrictShader(ctx, image); if (!shader) { auto [pixels, origin] = this->resolve( ctx.withNewDesiredOutput(image.layerBounds()), image); shader = FilterResultTestAccess::StrictShader( ctx, FilterResult(std::move(pixels), LayerSpace(origin))); } } SkPaint paint; paint.setShader(std::move(shader)); canvas.drawPaint(paint); } else { SkASSERT(fMethod == Method::kDrawToCanvas); FilterResultTestAccess::Draw(ctx, device.get(), image, /*preserveDeviceState=*/false); } return {device->snapSpecial(SkIRect::MakeWH(ctx.desiredOutput().width(), ctx.desiredOutput().height())), SkIPoint(ctx.desiredOutput().topLeft())}; } } private: Method fMethod; }; class TestRunner { static constexpr SkColorType kColorType = kRGBA_8888_SkColorType; using ResolveMethod = FilterResultImageResolver::Method; public: // Raster-backed TestRunner TestRunner(skiatest::Reporter* reporter) : fReporter(reporter) , fBackend(skif::MakeRasterBackend(/*surfaceProps=*/{}, kColorType)) {} // Ganesh-backed TestRunner #if defined(SK_GANESH) TestRunner(skiatest::Reporter* reporter, GrDirectContext* context) : fReporter(reporter) , fDirectContext(context) , fBackend(skif::MakeGaneshBackend(sk_ref_sp(context), kTopLeft_GrSurfaceOrigin, /*surfaceProps=*/{}, kColorType)) {} #endif // Graphite-backed TestRunner #if defined(SK_GRAPHITE) TestRunner(skiatest::Reporter* reporter, skgpu::graphite::Recorder* recorder) : fReporter(reporter) , fRecorder(recorder) , fBackend(skif::MakeGraphiteBackend(recorder, /*surfaceProps=*/{}, kColorType)) {} #endif // Let TestRunner be passed in to places that take a Reporter* or to REPORTER_ASSERT etc. operator skiatest::Reporter*() const { return fReporter; } skiatest::Reporter* operator->() const { return fReporter; } skif::Backend* backend() const { return fBackend.get(); } sk_sp refBackend() const { return fBackend; } bool compareImages(const skif::Context& ctx, SkSpecialImage* expectedImage, SkIPoint expectedOrigin, const FilterResult& actual, float allowedPercentImageDiff, int transparentCheckBorderTolerance) { if (!expectedImage) { // For pathological desired outputs, we can't actually produce an expected image so // just carry on w/o validating. return true; } SkBitmap expectedBM = this->readPixels(expectedImage); // Resolve actual using all 4 methods to ensure they are approximately equal to the expected // (which is used as a proxy for being approximately equal to each other). return this->compareImages(ctx, expectedBM, expectedOrigin, actual, ResolveMethod::kImageAndOffset, allowedPercentImageDiff, transparentCheckBorderTolerance) && this->compareImages(ctx, expectedBM, expectedOrigin, actual, ResolveMethod::kDrawToCanvas, allowedPercentImageDiff, transparentCheckBorderTolerance) && this->compareImages(ctx, expectedBM, expectedOrigin, actual, ResolveMethod::kShader, allowedPercentImageDiff, transparentCheckBorderTolerance) && this->compareImages(ctx, expectedBM, expectedOrigin, actual, ResolveMethod::kClippedShader, allowedPercentImageDiff, transparentCheckBorderTolerance); } bool validateOptimizedImage(const skif::Context& ctx, const FilterResult& actual) { FilterResultImageResolver expectedResolver{ResolveMethod::kStrictShader}; auto [expectedImage, expectedOrigin] = expectedResolver.resolve(ctx, actual); SkBitmap expectedBM = this->readPixels(expectedImage.get()); return this->compareImages(ctx, expectedBM, expectedOrigin, actual, ResolveMethod::kImageAndOffset, /*allowedPercentImageDiff=*/0.0f, /*transparentCheckBorderTolerance=*/0); } sk_sp createSourceImage(SkISize size, sk_sp colorSpace) { sk_sp sourceSurface = fBackend->makeDevice(size, std::move(colorSpace)); const SkColor colors[] = { SK_ColorMAGENTA, SK_ColorRED, SK_ColorYELLOW, SK_ColorGREEN, SK_ColorCYAN, SK_ColorBLUE }; SkMatrix rotation = SkMatrix::RotateDeg(15.f, {size.width() / 2.f, size.height() / 2.f}); SkCanvas canvas{sourceSurface}; canvas.clear(SK_ColorBLACK); canvas.concat(rotation); int color = 0; SkRect coverBounds; SkRect dstBounds = SkRect::Make(canvas.imageInfo().bounds()); SkAssertResult(SkMatrixPriv::InverseMapRect(rotation, &coverBounds, dstBounds)); float sz = size.width() <= 16.f || size.height() <= 16.f ? 2.f : 8.f; for (float y = coverBounds.fTop; y < coverBounds.fBottom; y += sz) { for (float x = coverBounds.fLeft; x < coverBounds.fRight; x += sz) { SkPaint p; p.setColor(colors[(color++) % std::size(colors)]); canvas.drawRect(SkRect::MakeXYWH(x, y, sz, sz), p); } } return sourceSurface->snapSpecial(SkIRect::MakeSize(size)); } private: bool compareImages(const skif::Context& ctx, const SkBitmap& expected, SkIPoint expectedOrigin, const FilterResult& actual, ResolveMethod method, float allowedPercentImageDiff, int transparentCheckBorderTolerance) { FilterResultImageResolver resolver{method}; auto [actualImage, actualOrigin] = resolver.resolve(ctx, actual); SkBitmap actualBM = this->readPixels(actualImage.get()); // empty if actualImage is null TArray badPixels; if (!this->compareBitmaps(expected, expectedOrigin, actualBM, actualOrigin, allowedPercentImageDiff, transparentCheckBorderTolerance, &badPixels)) { if (!fLoggedErrorImage) { SkDebugf("FilterResult comparison failed for method %s\n", resolver.methodName()); this->logBitmaps(expected, actualBM, badPixels); fLoggedErrorImage = true; } return false; } else if (kLogAllBitmaps) { this->logBitmaps(expected, actualBM, badPixels); } return true; } bool compareBitmaps(const SkBitmap& expected, SkIPoint expectedOrigin, const SkBitmap& actual, SkIPoint actualOrigin, float allowedPercentImageDiff, int transparentCheckBorderTolerance, TArray* badPixels) { SkIRect excludeTransparentCheck; // region in expectedBM that can be non-transparent if (actual.empty()) { // A null image in a FilterResult is equivalent to transparent black, so we should // expect the contents of 'expectedImage' to be transparent black. excludeTransparentCheck = SkIRect::MakeEmpty(); } else { // The actual image bounds should be contained in the expected image's bounds. SkIRect actualBounds = SkIRect::MakeXYWH(actualOrigin.x(), actualOrigin.y(), actual.width(), actual.height()); SkIRect expectedBounds = SkIRect::MakeXYWH(expectedOrigin.x(), expectedOrigin.y(), expected.width(), expected.height()); const bool contained = expectedBounds.contains(actualBounds); REPORTER_ASSERT(fReporter, contained, "actual image [%d %d %d %d] not contained within expected [%d %d %d %d]", actualBounds.fLeft, actualBounds.fTop, actualBounds.fRight, actualBounds.fBottom, expectedBounds.fLeft, expectedBounds.fTop, expectedBounds.fRight, expectedBounds.fBottom); if (!contained) { return false; } // The actual pixels should match fairly closely with the expected, allowing for minor // differences from consolidating actions into a single render, etc. int errorCount = 0; SkIPoint offset = actualOrigin - expectedOrigin; for (int y = 0; y < actual.height(); ++y) { for (int x = 0; x < actual.width(); ++x) { SkIPoint ep = {x + offset.x(), y + offset.y()}; SkColor4f expectedColor = expected.getColor4f(ep.fX, ep.fY); SkColor4f actualColor = actual.getColor4f(x, y); if (actualColor != expectedColor && !this->approxColor(this->boxFilter(actual, x, y), this->boxFilter(expected, ep.fX, ep.fY))) { badPixels->push_back(ep); errorCount++; } } } const int totalCount = expected.width() * expected.height(); const float percentError = 100.f * errorCount / (float) totalCount; const bool approxMatch = percentError <= allowedPercentImageDiff; REPORTER_ASSERT(fReporter, approxMatch, "%d pixels were too different from %d total (%f %% vs. %f %%)", errorCount, totalCount, percentError, allowedPercentImageDiff); if (!approxMatch) { return false; } // The expected pixels outside of the actual bounds should be transparent, otherwise // the actual image is not returning enough data. excludeTransparentCheck = actualBounds.makeOffset(-expectedOrigin); // Add per-test padding to the exclusion, which is used when there is upscaling in the // expected image that bleeds beyond the layer bounds, but is hard to enforce in the // simplified expectation rendering. excludeTransparentCheck.outset(transparentCheckBorderTolerance, transparentCheckBorderTolerance); } int badTransparencyCount = 0; for (int y = 0; y < expected.height(); ++y) { for (int x = 0; x < expected.width(); ++x) { if (!excludeTransparentCheck.isEmpty() && excludeTransparentCheck.contains(x, y)) { continue; } // If we are on the edge of the transparency exclusion bounds, allow pixels to be // up to 2 off to account for sloppy GPU rendering (seen on some Android devices). // This is still visually "transparent" and definitely make sure that // off-transparency does not extend across the entire surface (tolerance = 0). const bool onEdge = !excludeTransparentCheck.isEmpty() && excludeTransparentCheck.makeOutset(1, 1).contains(x, y); if (!this->approxColor(expected.getColor4f(x, y), SkColors::kTransparent, onEdge ? kAATolerance : 0.f)) { badPixels->push_back({x, y}); badTransparencyCount++; } } } REPORTER_ASSERT(fReporter, badTransparencyCount == 0, "Unexpected non-transparent pixels"); return badTransparencyCount == 0; } bool approxColor(const SkColor4f& a, const SkColor4f& b, float tolerance = kRGBTolerance) const { SkPMColor4f apm = a.premul(); SkPMColor4f bpm = b.premul(); // Calculate red-mean, a lowcost approximation of color difference that gives reasonable // results for the types of acceptable differences resulting from collapsing compatible // SkSamplingOptions or slightly different AA on shape boundaries. // See https://www.compuphase.com/cmetric.htm float r = (apm.fR + bpm.fR) / 2.f; float dr = (apm.fR - bpm.fR); float dg = (apm.fG - bpm.fG); float db = (apm.fB - bpm.fB); float delta = sqrt((2.f + r)*dr*dr + 4.f*dg*dg + (2.f + (1.f - r))*db*db); return delta <= tolerance; } SkColor4f boxFilter(const SkBitmap& bm, int x, int y) const { static constexpr int kKernelOffset = kKernelSize / 2; SkPMColor4f sum = {0.f, 0.f, 0.f, 0.f}; float netWeight = 0.f; for (int sy = y - kKernelOffset; sy <= y + kKernelOffset; ++sy) { for (int sx = x - kKernelOffset; sx <= x + kKernelOffset; ++sx) { float weight = kFuzzyKernel[sy - y + kKernelOffset][sx - x + kKernelOffset]; if (sx < 0 || sx >= bm.width() || sy < 0 || sy >= bm.height()) { // Treat outside image as transparent black, this is necessary to get // consistent comparisons between expected and actual images where the actual // is cropped as tightly as possible. netWeight += weight; continue; } SkPMColor4f c = bm.getColor4f(sx, sy).premul() * weight; sum.fR += c.fR; sum.fG += c.fG; sum.fB += c.fB; sum.fA += c.fA; netWeight += weight; } } SkASSERT(netWeight > 0.f); return sum.unpremul() * (1.f / netWeight); } SkBitmap readPixels(const SkSpecialImage* specialImage) const { if (!specialImage) { return SkBitmap(); // an empty bitmap } [[maybe_unused]] int srcX = specialImage->subset().fLeft; [[maybe_unused]] int srcY = specialImage->subset().fTop; SkImageInfo ii = SkImageInfo::Make(specialImage->dimensions(), specialImage->colorInfo()); SkBitmap bm; bm.allocPixels(ii); #if defined(SK_GANESH) if (fDirectContext) { // Ganesh backed, just use the SkImage::readPixels API SkASSERT(specialImage->isGaneshBacked()); sk_sp image = specialImage->asImage(); SkAssertResult(image->readPixels(fDirectContext, bm.pixmap(), srcX, srcY)); } else #endif #if defined(SK_GRAPHITE) if (fRecorder) { // Graphite backed, so use the private testing-only synchronous API SkASSERT(specialImage->isGraphiteBacked()); auto view = skgpu::graphite::AsView(specialImage->asImage()); auto proxyII = ii.makeWH(view.width(), view.height()); SkAssertResult(fRecorder->priv().context()->priv().readPixels( bm.pixmap(), view.proxy(), proxyII, srcX, srcY)); } else #endif { // Assume it's raster backed, so use AsBitmap directly SkAssertResult(SkSpecialImages::AsBitmap(specialImage, &bm)); } return bm; } void logBitmaps(const SkBitmap& expected, const SkBitmap& actual, const TArray& badPixels) { SkString expectedURL; ToolUtils::BitmapToBase64DataURI(expected, &expectedURL); SkDebugf("Expected:\n%s\n\n", expectedURL.c_str()); if (!actual.empty()) { SkString actualURL; ToolUtils::BitmapToBase64DataURI(actual, &actualURL); SkDebugf("Actual:\n%s\n\n", actualURL.c_str()); } else { SkDebugf("Actual: null (fully transparent)\n\n"); } if (!badPixels.empty()) { SkBitmap error = expected; error.allocPixels(); SkAssertResult(expected.readPixels(error.pixmap())); for (auto p : badPixels) { error.erase(SkColors::kRed, SkIRect::MakeXYWH(p.fX, p.fY, 1, 1)); } SkString markedURL; ToolUtils::BitmapToBase64DataURI(error, &markedURL); SkDebugf("Errors:\n%s\n\n", markedURL.c_str()); } } skiatest::Reporter* fReporter; #if defined(SK_GANESH) GrDirectContext* fDirectContext = nullptr; #endif #if defined(SK_GRAPHITE) skgpu::graphite::Recorder* fRecorder = nullptr; #endif sk_sp fBackend; bool fLoggedErrorImage = false; // only do this once per test runner }; class TestCase { public: TestCase(TestRunner& runner, std::string name, float allowedPercentImageDiff=kDefaultMaxAllowedPercentImageDiff, int transparentCheckBorderTolerance=0) : fRunner(runner) , fName(name) , fAllowedPercentImageDiff(allowedPercentImageDiff) , fTransparentCheckBorderTolerance(transparentCheckBorderTolerance) , fSourceBounds(LayerSpace::Empty()) , fDesiredOutput(LayerSpace::Empty()) {} TestCase& source(const SkIRect& bounds) { fSourceBounds = LayerSpace(bounds); return *this; } TestCase& applyCrop(const SkIRect& crop, Expect expectation) { return this->applyCrop(crop, SkTileMode::kDecal, expectation); } TestCase& applyCrop(const SkIRect& crop, SkTileMode tileMode, Expect expectation, std::optional expectedTileMode = {}, std::optional expectedBounds = {}) { // Fill-in automated expectations, which is to equal 'tileMode' when not overridden. if (!expectedTileMode) { expectedTileMode = tileMode; } std::optional> expectedLayerBounds; if (expectedBounds) { expectedLayerBounds = LayerSpace(*expectedBounds); } fActions.emplace_back(crop, tileMode, expectedLayerBounds, expectation, this->getDefaultExpectedSampling(expectation), *expectedTileMode, this->getDefaultExpectedColorFilter(expectation)); return *this; } TestCase& applyTransform(const SkMatrix& matrix, Expect expectation) { return this->applyTransform(matrix, FilterResult::kDefaultSampling, expectation); } TestCase& applyTransform(const SkMatrix& matrix, const SkSamplingOptions& sampling, Expect expectation, std::optional expectedSampling = {}) { // Fill-in automated expectations, which is simply that if it's not explicitly provided we // assume the result's sampling equals what was passed to applyTransform(). if (!expectedSampling.has_value()) { expectedSampling = sampling; } fActions.emplace_back(matrix, sampling, expectation, *expectedSampling, this->getDefaultExpectedTileMode(expectation, /*cfAffectsTransparency=*/false), this->getDefaultExpectedColorFilter(expectation)); return *this; } TestCase& applyColorFilter(sk_sp colorFilter, Expect expectation, std::optional> expectedColorFilter = {}) { // The expected color filter is the composition of the default expectation (e.g. last // color filter or null for a new image) and the new 'colorFilter'. Compose() automatically // returns 'colorFilter' if the inner filter is null. if (!expectedColorFilter.has_value()) { expectedColorFilter = SkColorFilters::Compose( colorFilter, this->getDefaultExpectedColorFilter(expectation)); } const bool affectsTransparent = as_CFB(colorFilter)->affectsTransparentBlack(); fActions.emplace_back(std::move(colorFilter), expectation, this->getDefaultExpectedSampling(expectation), this->getDefaultExpectedTileMode(expectation, affectsTransparent), std::move(*expectedColorFilter)); return *this; } TestCase& rescale(SkSize scale, Expect expectation, std::optional expectedTileMode = {}) { SkASSERT(!fActions.empty()); if (!expectedTileMode) { expectedTileMode = this->getDefaultExpectedTileMode(expectation, /*cfAffectsTransparency=*/false); } fActions.emplace_back(skif::LayerSpace(scale), expectation, this->getDefaultExpectedSampling(expectation), *expectedTileMode, this->getDefaultExpectedColorFilter(expectation)); return *this; } void run(const SkIRect& requestedOutput) const { skiatest::ReporterContext caseLabel(fRunner, fName); this->run(requestedOutput, /*backPropagateDesiredOutput=*/true); this->run(requestedOutput, /*backPropagateDesiredOutput=*/false); } void run(const SkIRect& requestedOutput, bool backPropagateDesiredOutput) const { SkASSERT(!fActions.empty()); // It's a bad test case if there aren't any actions skiatest::ReporterContext backPropagate( fRunner, SkStringPrintf("backpropagate output: %d", backPropagateDesiredOutput)); auto desiredOutput = LayerSpace(requestedOutput); std::vector> desiredOutputs; desiredOutputs.resize(fActions.size(), desiredOutput); if (!backPropagateDesiredOutput) { // Set the desired output to be equal to the expected output so that there is no // further restriction of what's computed for early actions to then be ruled out by // subsequent actions. auto inputBounds = fSourceBounds; for (int i = 0; i < (int) fActions.size() - 1; ++i) { desiredOutputs[i] = fActions[i].expectedBounds(inputBounds); // If the output for the ith action is infinite, leave it for now and expand the // input bounds for action i+1. The infinite bounds will be replaced by the // back-propagated desired output of the next action. if (SkIRect(desiredOutputs[i]) == SkRectPriv::MakeILarge()) { inputBounds.outset(LayerSpace({25, 25})); } else { inputBounds = desiredOutputs[i]; } } } // Fill out regular back-propagated desired outputs and cleanup infinite outputs for (int i = (int) fActions.size() - 2; i >= 0; --i) { if (backPropagateDesiredOutput || SkIRect(desiredOutputs[i]) == SkRectPriv::MakeILarge()) { desiredOutputs[i] = fActions[i+1].requiredInput(desiredOutputs[i+1]); } } // Create the source image sk_sp colorSpace = SkColorSpace::MakeSRGB(); FilterResult source; if (!fSourceBounds.isEmpty()) { source = FilterResult(fRunner.createSourceImage(SkISize(fSourceBounds.size()), colorSpace), fSourceBounds.topLeft()); } Context baseContext{fRunner.refBackend(), skif::Mapping{SkM44()}, skif::LayerSpace::Empty(), source, colorSpace.get(), /*stats=*/nullptr}; // Applying modifiers to FilterResult might produce a new image, but hopefully it's // able to merge properties and even re-order operations to minimize the number of offscreen // surfaces that it creates. To validate that this is producing an equivalent image, we // track what to expect by rendering each action every time without any optimization. sk_sp expectedImage = source.refImage(); LayerSpace expectedOrigin = source.layerBounds().topLeft(); // The expected image can't ever be null, so we produce a transparent black image instead. if (!expectedImage) { sk_sp expectedSurface = fRunner.backend()->makeDevice({1, 1}, colorSpace); clear_device(expectedSurface.get()); expectedImage = expectedSurface->snapSpecial(SkIRect::MakeWH(1, 1)); expectedOrigin = LayerSpace({0, 0}); } SkASSERT(expectedImage); // Apply each action and validate, from first to last action for (int i = 0; i < (int) fActions.size(); ++i) { skiatest::ReporterContext actionLabel(fRunner, SkStringPrintf("action %d", i)); Stats stats; auto ctx = baseContext.withNewDesiredOutput(desiredOutputs[i]); FilterResultTestAccess::TrackStats(&ctx, &stats); FilterResult output = fActions[i].apply(ctx, source); // Validate consistency of the output REPORTER_ASSERT(fRunner, SkToBool(output.image()) == !output.layerBounds().isEmpty()); LayerSpace expectedBounds = fActions[i].expectedBounds(source.layerBounds()); Expect correctedExpectation = fActions[i].expectation(); if (SkIRect(expectedBounds) == SkRectPriv::MakeILarge()) { // An expected image filling out to infinity should have an actual image that // fills the desired output. expectedBounds = desiredOutputs[i]; if (desiredOutputs[i].isEmpty()) { correctedExpectation = Expect::kEmptyImage; } } else if (!expectedBounds.intersect(desiredOutputs[i])) { // Test cases should provide image expectations for the case where desired output // is not back-propagated. When desired output is back-propagated, it can lead to // earlier actions becoming empty actions. REPORTER_ASSERT(fRunner, fActions[i].expectation() == Expect::kEmptyImage || backPropagateDesiredOutput); expectedBounds = LayerSpace::Empty(); correctedExpectation = Expect::kEmptyImage; } std::vector allowedOffscreenSurfaces = fActions[i].expectedOffscreenSurfaces(source); int actualShaderDraws = stats.fNumShaderBasedTilingDraws + stats.fNumShaderClampedDraws; int expectedShaderTiledDraws = 0; bool actualNewImage = output.image() && (!source.image() || output.image()->uniqueID() != source.image()->uniqueID()); switch(correctedExpectation) { case Expect::kNewImage: REPORTER_ASSERT(fRunner, actualNewImage); if (source && !source.image()->isExactFit()) { // Even if we're rescaling and making multiple surfaces, shader tiling // should only ever be needed on the first step. expectedShaderTiledDraws = std::min(1, allowedOffscreenSurfaces[0]); } break; case Expect::kDeferredImage: REPORTER_ASSERT(fRunner, !actualNewImage && output.image()); break; case Expect::kEmptyImage: REPORTER_ASSERT(fRunner, !actualNewImage && !output.image()); break; } // Verify stats behavior for the current action REPORTER_ASSERT(fRunner, find(allowedOffscreenSurfaces.begin(), allowedOffscreenSurfaces.end(), stats.fNumOffscreenSurfaces) != allowedOffscreenSurfaces.end(), "expected %d or %d, got %d", allowedOffscreenSurfaces[0], allowedOffscreenSurfaces.size() > 1 ? allowedOffscreenSurfaces[1] : -1, stats.fNumOffscreenSurfaces); REPORTER_ASSERT(fRunner, actualShaderDraws <= expectedShaderTiledDraws, "expected %d+%d <= %d", stats.fNumShaderBasedTilingDraws, stats.fNumShaderClampedDraws, expectedShaderTiledDraws); using ShaderSampleMode = FilterResultTestAccess::ShaderSampleMode; auto expectedSampleMode = fActions[i].expectedSampleMode(ctx, source); REPORTER_ASSERT(fRunner, stats.fNumShaderBasedTilingDraws == 0 || expectedSampleMode == ShaderSampleMode::kShaderTile); REPORTER_ASSERT(fRunner, stats.fNumShaderClampedDraws == 0 || expectedSampleMode == ShaderSampleMode::kShaderClamp); // Validate layer bounds and sampling when we expect a new or deferred image if (output.image()) { auto actualBounds = output.layerBounds(); // A deferred action doesn't have to crop its layer bounds to the desired output to // preserve accuracy of later bounds analysis. New images however should restrict // themselves to the desired output to minimize memory of the surface. The exception // is a new image for applyTransform() because the new transform is deferred to the // resolved image, which can make its layer bounds larger than the desired output. if (correctedExpectation == Expect::kDeferredImage || !FilterResultTestAccess::IsIntegerTransform(output)) { // Skip the check if the desiredOutputs's SkIRect reports empty. // LayerSpace won't be empty but since the W/H don't fit into 32-bit // SkIRect::intersect() will report false. REPORTER_ASSERT(fRunner, SkIRect(desiredOutputs[i]).isEmpty() || actualBounds.intersect(desiredOutputs[i])); } REPORTER_ASSERT(fRunner, !expectedBounds.isEmpty()); REPORTER_ASSERT(fRunner, SkIRect(actualBounds) == SkIRect(expectedBounds)); REPORTER_ASSERT(fRunner, output.sampling() == fActions[i].expectedSampling()); REPORTER_ASSERT(fRunner, output.tileMode() == fActions[i].expectedTileMode()); REPORTER_ASSERT(fRunner, colorfilter_equals(output.colorFilter(), fActions[i].expectedColorFilter())); if (actualShaderDraws < expectedShaderTiledDraws || (source.tileMode() != SkTileMode::kClamp && stats.fNumShaderClampedDraws > 0)) { // Some tile draws were optimized to HW draws, or some tile draws were reduced // to shader-clamped draws, so compare the output to a non-optimized image. REPORTER_ASSERT(fRunner, fRunner.validateOptimizedImage(ctx, output)); } } expectedImage = fActions[i].renderExpectedImage(ctx, std::move(expectedImage), expectedOrigin, desiredOutputs[i]); expectedOrigin = desiredOutputs[i].topLeft(); if (!fRunner.compareImages(ctx, expectedImage.get(), SkIPoint(expectedOrigin), output, fAllowedPercentImageDiff, fTransparentCheckBorderTolerance)) { // If one iteration is incorrect, its failures will likely cascade to further // actions so end now as the test has failed. break; } source = output; } } private: // By default an action that doesn't define its own sampling options will not change sampling // unless it produces a new image. Otherwise it inherits the prior action's expectation. SkSamplingOptions getDefaultExpectedSampling(Expect expectation) const { if (expectation != Expect::kDeferredImage || fActions.empty()) { return FilterResult::kDefaultSampling; } else { return fActions[fActions.size() - 1].expectedSampling(); } } // By default an action that doesn't define its own tiling will not change the tiling, unless it // produces a new image, at which point it becomes kDecal again. SkTileMode getDefaultExpectedTileMode(Expect expectation, bool cfAffectsTransparency) const { if (expectation == Expect::kNewImage && cfAffectsTransparency) { return SkTileMode::kClamp; } else if (expectation != Expect::kDeferredImage || fActions.empty()) { return SkTileMode::kDecal; } else { return fActions[fActions.size() - 1].expectedTileMode(); } } // By default an action that doesn't define its own color filter will not change filtering, // unless it produces a new image. Otherwise it inherits the prior action's expectations. sk_sp getDefaultExpectedColorFilter(Expect expectation) const { if (expectation != Expect::kDeferredImage || fActions.empty()) { return nullptr; } else { return sk_ref_sp(fActions[fActions.size() - 1].expectedColorFilter()); } } TestRunner& fRunner; std::string fName; float fAllowedPercentImageDiff; int fTransparentCheckBorderTolerance; // Used to construct an SkSpecialImage of the given size/location filled with the known pattern. LayerSpace fSourceBounds; // The intended area to fill with the result, controlled by outside factors (e.g. clip bounds) LayerSpace fDesiredOutput; std::vector fActions; }; // ---------------------------------------------------------------------------- // Utilities to create color filters for the unit tests sk_sp alpha_modulate(float v) { // dst-in blending with src = (1,1,1,v) = dst * v auto cf = SkColorFilters::Blend({1.f,1.f,1.f,v}, /*colorSpace=*/nullptr, SkBlendMode::kDstIn); SkASSERT(cf && !as_CFB(cf)->affectsTransparentBlack()); return cf; } sk_sp affect_transparent(SkColor4f color) { auto cf = SkColorFilters::Blend(color, /*colorSpace=*/nullptr, SkBlendMode::kPlus); SkASSERT(cf && as_CFB(cf)->affectsTransparentBlack()); return cf; } // ---------------------------------------------------------------------------- // TODO(skbug.com/14607) - Run FilterResultTests on Dawn and ANGLE backends, too #if defined(SK_GANESH) #define DEF_GANESH_TEST_SUITE(name, ctsEnforcement) \ DEF_GANESH_TEST_FOR_CONTEXTS(FilterResult_ganesh_##name, \ skgpu::IsNativeBackend, \ r, \ ctxInfo, \ nullptr, \ ctsEnforcement) { \ TestRunner runner(r, ctxInfo.directContext()); \ test_suite_##name(runner); \ } #else #define DEF_GANESH_TEST_SUITE(name) // do nothing #endif #if defined(SK_GRAPHITE) #define DEF_GRAPHITE_TEST_SUITE(name, ctsEnforcement) \ DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(FilterResult_graphite_##name, \ skgpu::IsNativeBackend, \ r, \ context, \ testContext, \ true, \ ctsEnforcement) { \ using namespace skgpu::graphite; \ auto recorder = context->makeRecorder(); \ TestRunner runner(r, recorder.get()); \ test_suite_##name(runner); \ std::unique_ptr recording = recorder->snap(); \ if (!recording) { \ ERRORF(r, "Failed to make recording"); \ return; \ } \ InsertRecordingInfo insertInfo; \ insertInfo.fRecording = recording.get(); \ context->insertRecording(insertInfo); \ testContext->syncedSubmit(context); \ } #else #define DEF_GRAPHITE_TEST_SUITE(name) // do nothing #endif #define DEF_TEST_SUITE(name, runner, ganeshCtsEnforcement, graphiteCtsEnforcement) \ static void test_suite_##name(TestRunner&); \ /* TODO(b/274901800): Uncomment to enable Graphite test execution. */ \ /* DEF_GRAPHITE_TEST_SUITE(name, graphiteCtsEnforcement) */ \ DEF_GANESH_TEST_SUITE(name, ganeshCtsEnforcement) \ DEF_TEST(FilterResult_raster_##name, reporter) { \ TestRunner runner(reporter); \ test_suite_##name(runner); \ } \ void test_suite_##name(TestRunner& runner) // ---------------------------------------------------------------------------- // Empty input/output tests DEF_TEST_SUITE(EmptySource, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // This is testing that an empty input image is handled by the applied actions without having // to generate new images, or that it can produce a new image from nothing when it affects // transparent black. for (SkTileMode tm : kTileModes) { TestCase(r, "applyCrop() to empty source") .source(SkIRect::MakeEmpty()) .applyCrop({0, 0, 10, 10}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 20, 20}); } TestCase(r, "applyTransform() to empty source") .source(SkIRect::MakeEmpty()) .applyTransform(SkMatrix::Translate(10.f, 10.f), Expect::kEmptyImage) .run(/*requestedOutput=*/{10, 10, 20, 20}); TestCase(r, "applyColorFilter() to empty source") .source(SkIRect::MakeEmpty()) .applyColorFilter(alpha_modulate(0.5f), Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); TestCase(r, "Transparency-affecting color filter overrules empty source") .source(SkIRect::MakeEmpty()) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kNewImage, /*expectedColorFilter=*/nullptr) // CF applied ASAP to make a new img .run(/*requestedOutput=*/{0, 0, 10, 10}); } DEF_TEST_SUITE(EmptyDesiredOutput, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // This is testing that an empty requested output is propagated through the applied actions so // that no actual images are generated. for (SkTileMode tm : kTileModes) { TestCase(r, "applyCrop() + empty output becomes empty") .source({0, 0, 10, 10}) .applyCrop({2, 2, 8, 8}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/SkIRect::MakeEmpty()); } TestCase(r, "applyTransform() + empty output becomes empty") .source({0, 0, 10, 10}) .applyTransform(SkMatrix::RotateDeg(10.f), Expect::kEmptyImage) .run(/*requestedOutput=*/SkIRect::MakeEmpty()); TestCase(r, "applyColorFilter() + empty output becomes empty") .source({0, 0, 10, 10}) .applyColorFilter(alpha_modulate(0.5f), Expect::kEmptyImage) .run(/*requestedOutput=*/SkIRect::MakeEmpty()); TestCase(r, "Transpency-affecting color filter + empty output is empty") .source({0, 0, 10, 10}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kEmptyImage) .run(/*requestedOutput=*/SkIRect::MakeEmpty()); } // ---------------------------------------------------------------------------- // applyCrop() tests DEF_TEST_SUITE(Crop, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // This is testing all the combinations of how the src, crop, and requested output rectangles // can interact while still resulting in a deferred image. The exception is non-decal tile // modes where the crop rect includes transparent pixels not filled by the source, which // requires a new image to ensure tiling matches the crop geometry. for (SkTileMode tm : kTileModes) { const Expect nonDecalExpectsNewImage = tm == SkTileMode::kDecal ? Expect::kDeferredImage : Expect::kNewImage; TestCase(r, "applyCrop() contained in source and output") .source({0, 0, 20, 20}) .applyCrop({8, 8, 12, 12}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{4, 4, 16, 16}); TestCase(r, "applyCrop() contained in source, intersects output") .source({0, 0, 20, 20}) .applyCrop({4, 4, 12, 12}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{8, 8, 16, 16}); TestCase(r, "applyCrop() intersects source, contained in output") .source({10, 10, 20, 20}) .applyCrop({4, 4, 16, 16}, tm, nonDecalExpectsNewImage) .run(/*requestedOutput=*/{0, 0, 20, 20}); TestCase(r, "applyCrop() intersects source and output") .source({0, 0, 10, 10}) .applyCrop({5, -5, 15, 5}, tm, nonDecalExpectsNewImage) .run(/*requestedOutput=*/{7, -2, 12, 8}); TestCase(r, "applyCrop() contains source, intersects output") .source({4, 4, 16, 16}) .applyCrop({0, 0, 20, 20}, tm, nonDecalExpectsNewImage) .run(/*requestedOutput=*/{-5, -5, 18, 18}); // In these cases, cropping with a non-decal tile mode can be discarded because the output // bounds are entirely within the crop so no tiled edges would be visible. TestCase(r, "applyCrop() intersects source, contains output") .source({0, 0, 20, 20}) .applyCrop({-5, 5, 25, 15}, tm, Expect::kDeferredImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 5, 20, 15}); TestCase(r, "applyCrop() contains source and output") .source({0, 0, 10, 10}) .applyCrop({-5, -5, 15, 15}, tm, Expect::kDeferredImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{1, 1, 9, 9}); } } DEF_TEST_SUITE(CropDisjointFromSourceAndOutput, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // This tests all the combinations of src, crop, and requested output rectangles that result in // an empty image without any of the rectangles being empty themselves. The exception is for // non-decal tile modes when the source and crop still intersect. In that case the non-empty // content is tiled into the disjoint output rect, producing a non-empty image. for (SkTileMode tm : kTileModes) { TestCase(r, "applyCrop() disjoint from source, intersects output") .source({0, 0, 10, 10}) .applyCrop({11, 11, 20, 20}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 15, 15}); TestCase(r, "applyCrop() disjoint from source, intersects output disjoint from source") .source({0, 0, 10, 10}) .applyCrop({11, 11, 20, 20}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{12, 12, 18, 18}); TestCase(r, "applyCrop() disjoint from source and output") .source({0, 0, 10, 10}) .applyCrop({12, 12, 18, 18}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{-1, -1, 11, 11}); TestCase(r, "applyCrop() disjoint from source and output disjoint from source") .source({0, 0, 10, 10}) .applyCrop({-10, 10, -1, -1}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{11, 11, 20, 20}); // When the source and crop intersect but are disjoint from the output, the behavior depends // on the tile mode. For periodic tile modes, certain geometries can still be deferred by // conversion to a transform, but to keep expectations simple we pick bounds such that the // tiling can't be dropped. See PeriodicTileCrops for other scenarios. Expect nonDecalExpectsImage = tm == SkTileMode::kDecal ? Expect::kEmptyImage : tm == SkTileMode::kClamp ? Expect::kDeferredImage : Expect::kNewImage; TestCase(r, "applyCrop() intersects source, disjoint from output disjoint from source") .source({0, 0, 10, 10}) .applyCrop({-5, -5, 5, 5}, tm, nonDecalExpectsImage) .run(/*requestedOutput=*/{12, 12, 18, 18}); TestCase(r, "applyCrop() intersects source, disjoint from output") .source({0, 0, 10, 10}) .applyCrop({-5, -5, 5, 5}, tm, nonDecalExpectsImage) .run(/*requestedOutput=*/{6, 6, 18, 18}); } } DEF_TEST_SUITE(EmptyCrop, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "applyCrop() is empty") .source({0, 0, 10, 10}) .applyCrop(SkIRect::MakeEmpty(), tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); TestCase(r, "applyCrop() emptiness propagates") .source({0, 0, 10, 10}) .applyCrop({1, 1, 9, 9}, tm, Expect::kDeferredImage) .applyCrop(SkIRect::MakeEmpty(), tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); } } DEF_TEST_SUITE(DisjointCrops, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Disjoint applyCrop() after kDecal become empty") .source({0, 0, 10, 10}) .applyCrop({0, 0, 4, 4}, SkTileMode::kDecal, Expect::kDeferredImage) .applyCrop({6, 6, 10, 10}, tm, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); if (tm != SkTileMode::kDecal) { TestCase(r, "Disjoint tiling applyCrop() before kDecal is not empty and combines") .source({0, 0, 10, 10}) .applyCrop({0, 0, 4, 4}, tm, Expect::kDeferredImage) .applyCrop({6, 6, 10, 10}, SkTileMode::kDecal, Expect::kDeferredImage, tm) .run(/*requestedOutput=*/{0, 0, 10, 10}); TestCase(r, "Disjoint non-decal applyCrops() are not empty") .source({0, 0, 10, 10}) .applyCrop({0, 0, 4, 4}, tm, Expect::kDeferredImage) .applyCrop({6, 6, 10, 10}, tm, tm == SkTileMode::kClamp ? Expect::kDeferredImage : Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); } } } DEF_TEST_SUITE(IntersectingCrops, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Decal applyCrop() always combines with any other crop") .source({0, 0, 20, 20}) .applyCrop({5, 5, 15, 15}, tm, Expect::kDeferredImage) .applyCrop({10, 10, 20, 20}, SkTileMode::kDecal, Expect::kDeferredImage, tm) .run(/*requestedOutput=*/{0, 0, 20, 20}); if (tm != SkTileMode::kDecal) { TestCase(r, "Decal applyCrop() before non-decal crop requires new image") .source({0, 0, 20, 20}) .applyCrop({5, 5, 15, 15}, SkTileMode::kDecal, Expect::kDeferredImage) .applyCrop({10, 10, 20, 20}, tm, Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 20, 20}); TestCase(r, "Consecutive non-decal crops combine if both are clamp") .source({0, 0, 20, 20}) .applyCrop({5, 5, 15, 15}, tm, Expect::kDeferredImage) .applyCrop({10, 10, 20, 20}, tm, tm == SkTileMode::kClamp ? Expect::kDeferredImage : Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 20, 20}); } } } DEF_TEST_SUITE(PeriodicTileCrops, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : {SkTileMode::kRepeat, SkTileMode::kMirror}) { // In these tests, the crop periodically tiles such that it covers the desired output so // the prior image can be simply transformed. TestCase(r, "Periodic applyCrop() becomes a transform") .source({0, 0, 20, 20}) .applyCrop({5, 5, 15, 15}, tm, Expect::kDeferredImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{25, 25, 35, 35}); TestCase(r, "Periodic applyCrop() with partial transparency still becomes a transform") .source({0, 0, 20, 20}) .applyCrop({-5, -5, 15, 15}, tm, Expect::kDeferredImage, /*expectedTileMode=*/SkTileMode::kDecal, /*expectedBounds=*/tm == SkTileMode::kRepeat ? SkIRect{20,20,35,35} : SkIRect{15,15,30,30}) .run(/*requestedOutput*/{15, 15, 35, 35}); TestCase(r, "Periodic applyCrop() after complex transform can still simplify") .source({0, 0, 20, 20}) .applyTransform(SkMatrix::RotateDeg(15.f, {10.f, 10.f}), Expect::kDeferredImage) .applyCrop({-5, -5, 25, 25}, tm, Expect::kDeferredImage, /*expectedTileMode=*/SkTileMode::kDecal, /*expectedBounds*/SkIRect{57,57,83,83}) // source+15 degree rotation .run(/*requestedOutput=*/{55,55,85,85}); // In these tests, the crop's periodic boundary intersects with the output so it should not // simplify to just a transform. TestCase(r, "Periodic applyCrop() with visible edge does not become a transform") .source({0, 0, 20, 20}) .applyCrop({5, 5, 15, 15}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{10, 10, 20, 20}); TestCase(r, "Periodic applyCrop() with visible edge and transparency creates new image") .source({0, 0, 20, 20}) .applyCrop({-5, -5, 15, 15}, tm, Expect::kNewImage) .run(/*requestedOutput=*/{10, 10, 20, 20}); TestCase(r, "Periodic applyCrop() with visible edge and complex transform creates image") .source({0, 0, 20, 20}) .applyTransform(SkMatrix::RotateDeg(15.f, {10.f, 10.f}), Expect::kDeferredImage) .applyCrop({-5, -5, 25, 25}, tm, Expect::kNewImage) .run(/*requestedOutput=*/{20, 20, 50, 50}); // oss-fuzz:70128 ensure period calculations don't overflow (but will fail image creation) TestCase(r, "Pathologically large crop rect") .source({0, 0, 10, 10}) .applyCrop({0, 0, 1, 2}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(1.f, {5.f, 5.f}), Expect::kEmptyImage) .run(/*requestedOutput=*/{-726713344, 7, 1464866662, 15}); } } DEF_TEST_SUITE(DecalThenClamp, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Decal then clamp crop uses 1px buffer around intersection") .source({0, 0, 20, 20}) .applyCrop({3, 3, 17, 17}, SkTileMode::kDecal, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyCrop({3, 3, 20, 20}, SkTileMode::kClamp, Expect::kNewImage, SkTileMode::kClamp) .run(/*requestedOutput=*/{0, 0, 20, 20}); TestCase(r, "Decal then clamp crop uses 1px buffer around intersection, w/ alpha color filter") .source({0, 0, 20, 20}) .applyCrop({3, 3, 17, 17}, SkTileMode::kDecal, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kCyan), Expect::kDeferredImage) .applyCrop({0, 0, 17, 17}, SkTileMode::kClamp, Expect::kNewImage, SkTileMode::kClamp) .run(/*requestedOutput=*/{0, 0, 20, 20}); } // ---------------------------------------------------------------------------- // applyTransform() tests DEF_TEST_SUITE(Transform, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "applyTransform() integer translate") .source({0, 0, 10, 10}) .applyTransform(SkMatrix::Translate(5, 5), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); TestCase(r, "applyTransform() fractional translate") .source({0, 0, 10, 10}) .applyTransform(SkMatrix::Translate(1.5f, 3.24f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 10, 10}); TestCase(r, "applyTransform() scale") .source({0, 0, 24, 24}) .applyTransform(SkMatrix::Scale(2.2f, 3.1f), Expect::kDeferredImage) .run(/*requestedOutput=*/{-16, -16, 96, 96}); // NOTE: complex is anything beyond a scale+translate. See SkImageFilter_Base::MatrixCapability. TestCase(r, "applyTransform() with complex transform") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(10.f, {4.f, 4.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); } DEF_TEST_SUITE(CompatibleSamplingConcatsTransforms, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "linear + linear combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "equiv. bicubics combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "linear + bicubic becomes bicubic") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "bicubic + linear becomes bicubic") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage, /*expectedSampling=*/SkCubicResampler::Mitchell()) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "aniso picks max level to combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(4.f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(2.f), Expect::kDeferredImage, /*expectedSampling=*/SkSamplingOptions::Aniso(4.f)) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "aniso picks max level to combine (other direction)") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(2.f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(4.f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "linear + aniso becomes aniso") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(2.f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "aniso + linear stays aniso") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(4.f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage, /*expectedSampling=*/SkSamplingOptions::Aniso(4.f)) .run(/*requestedOutput=*/{0, 0, 16, 16}); // TODO: Add cases for mipmapping once that becomes relevant (SkSpecialImage does not have // mipmaps right now). } DEF_TEST_SUITE(IncompatibleSamplingResolvesImages, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "different bicubics do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::CatmullRom(), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "nearest + linear do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kNearest, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "linear + nearest do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kLinear, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kNearest, Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "bicubic + aniso do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(4.f), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "aniso + bicubic do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkSamplingOptions::Aniso(4.f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "nearest + nearest do not combine") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kNearest, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkFilterMode::kNearest, Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); } DEF_TEST_SUITE(IntegerOffsetIgnoresNearestSampling, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // Bicubic is used here to reflect that it should use the non-NN sampling and just needs to be // something other than the default to detect that it got carried through. TestCase(r, "integer translate+NN then bicubic combines") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::Translate(2, 2), SkFilterMode::kNearest, Expect::kDeferredImage, FilterResult::kDefaultSampling) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "bicubic then integer translate+NN combines") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::RotateDeg(2.f, {4.f, 4.f}), SkCubicResampler::Mitchell(), Expect::kDeferredImage) .applyTransform(SkMatrix::Translate(2, 2), SkFilterMode::kNearest, Expect::kDeferredImage, /*expectedSampling=*/SkCubicResampler::Mitchell()) .run(/*requestedOutput=*/{0, 0, 16, 16}); } // ---------------------------------------------------------------------------- // applyTransform() interacting with applyCrop() DEF_TEST_SUITE(TransformBecomesEmpty, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Transform moves src image outside of requested output") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::Translate(10.f, 10.f), Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 8, 8}); TestCase(r, "Transform moves src image outside of crop") .source({0, 0, 8, 8}) .applyTransform(SkMatrix::Translate(10.f, 10.f), Expect::kDeferredImage) .applyCrop({2, 2, 6, 6}, Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 20, 20}); TestCase(r, "Transform moves cropped image outside of requested output") .source({0, 0, 8, 8}) .applyCrop({1, 1, 4, 4}, Expect::kDeferredImage) .applyTransform(SkMatrix::Translate(-5.f, -5.f), Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 8, 8}); } DEF_TEST_SUITE(TransformAndCrop, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Crop after transform can always apply") .source({0, 0, 16, 16}) .applyTransform(SkMatrix::RotateDeg(45.f, {3.f, 4.f}), Expect::kDeferredImage) .applyCrop({2, 2, 15, 15}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); // TODO: Expand this test case to be arbitrary float S+T transforms when FilterResult tracks // both a srcRect and dstRect. TestCase(r, "Crop after translate is lifted to image subset") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::Translate(12.f, 8.f), Expect::kDeferredImage) .applyCrop({16, 16, 24, 24}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {16.f, 16.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transform after unlifted crop triggers new image") .source({0, 0, 16, 16}) .applyTransform(SkMatrix::RotateDeg(45.f, {8.f, 8.f}), Expect::kDeferredImage) .applyCrop({1, 1, 15, 15}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(-10.f, {8.f, 4.f}), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "Transform after unlifted crop with interior output does not trigger new image") .source({0, 0, 16, 16}) .applyTransform(SkMatrix::RotateDeg(45.f, {8.f, 8.f}), Expect::kDeferredImage) .applyCrop({1, 1, 15, 15}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(-10.f, {8.f, 4.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{4, 4, 12, 12}); TestCase(r, "Translate after unlifted crop does not trigger new image") .source({0, 0, 16, 16}) .applyTransform(SkMatrix::RotateDeg(5.f, {8.f, 8.f}), Expect::kDeferredImage) .applyCrop({2, 2, 14, 14}, Expect::kDeferredImage) .applyTransform(SkMatrix::Translate(4.f, 6.f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "Transform after large no-op crop does not trigger new image") .source({0, 0, 64, 64}) .applyTransform(SkMatrix::RotateDeg(45.f, {32.f, 32.f}), Expect::kDeferredImage) .applyCrop({-64, -64, 128, 128}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(-30.f, {32.f, 32.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 64, 64}); } DEF_TEST_SUITE(TransformAndTile, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // Test interactions of non-decal tile modes and transforms for (SkTileMode tm : kTileModes) { if (tm == SkTileMode::kDecal) { continue; } TestCase(r, "Transform after tile mode does not trigger new image") .source({0, 0, 64, 64}) .applyCrop({2, 2, 32, 32}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(20.f, {16.f, 8.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 64, 64}); TestCase(r, "Integer transform before tile mode does not trigger new image") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::Translate(16.f, 16.f), Expect::kDeferredImage) .applyCrop({20, 20, 40, 40}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 64, 64}); TestCase(r, "Non-integer transform before tile mode triggers new image") .source({0, 0, 50, 40}) .applyTransform(SkMatrix::RotateDeg(-30.f, {20.f, 10.f}), Expect::kDeferredImage) .applyCrop({10, 10, 30, 30}, tm, Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 50, 50}); TestCase(r, "Non-integer transform before tiling defers image if edges are hidden") .source({0, 0, 64, 64}) .applyTransform(SkMatrix::RotateDeg(45.f, {32.f, 32.f}), Expect::kDeferredImage) .applyCrop({10, 10, 50, 50}, tm, Expect::kDeferredImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{11, 11, 49, 49}); } } // ---------------------------------------------------------------------------- // applyColorFilter() and interactions with transforms/crops DEF_TEST_SUITE(ColorFilter, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "applyColorFilter() defers image") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "applyColorFilter() composes with other color filters") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transparency-affecting color filter fills output") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .run(/*requestedOutput=*/{-8, -8, 32, 32}); // Since there is no cropping between the composed color filters, transparency-affecting CFs // can still compose together. TestCase(r, "Transparency-affecting composition fills output (ATBx2)") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .run(/*requestedOutput=*/{-8, -8, 32, 32}); TestCase(r, "Transparency-affecting composition fills output (ATB,reg)") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{-8, -8, 32, 32}); TestCase(r, "Transparency-affecting composition fills output (reg,ATB)") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .run(/*requestedOutput=*/{-8, -8, 32, 32}); // oss-fuzz:70134 Requesting the output of an infinite image (e.g. transparency affecting // color filter) at the limit of 32-bit ints doesn't leave room for border padding pixels. // This should cleanly fail to an empty image. TestCase(r, "Pathologic output bounds with transparency-affecting color filter is empty") .source({0, 0, 16, 16}) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kEmptyImage) .run(/*requestedOutput=*/{-INT32_MAX, 0, -INT32_MAX + 10, 16}); } DEF_TEST_SUITE(TransformedColorFilter, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Transform composes with regular CF") .source({0, 0, 24, 24}) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Regular CF composes with transform") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Transform composes with transparency-affecting CF") .source({0, 0, 24, 24}) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); // NOTE: Because there is no explicit crop between the color filter and the transform, // output bounds propagation means the layer bounds of the applied color filter are never // visible post transform. This is detected and allows the transform to be composed without // producing an intermediate image. See later tests for when a crop prevents this optimization. TestCase(r, "Transparency-affecting CF composes with transform") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .run(/*requestedOutput=*/{-50, -50, 50, 50}); } DEF_TEST_SUITE(TransformBetweenColorFilters, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // NOTE: The lack of explicit crops allows all of these operations to be optimized as well. TestCase(r, "Transform between regular color filters") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.75f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Transform between transparency-affecting color filters") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Transform between ATB and regular color filters") .source({0, 0, 24, 24}) .applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.75f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Transform between regular and ATB color filters") .source({0, 0, 24, 24}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); } DEF_TEST_SUITE(ColorFilterBetweenTransforms, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Regular color filter between transforms") .source({0, 0, 24, 24}) .applyTransform(SkMatrix::RotateDeg(20.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.8f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(10.f, {5.f, 8.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); TestCase(r, "Transparency-affecting color filter between transforms") .source({0, 0, 24, 24}) .applyTransform(SkMatrix::RotateDeg(20.f, {12, 12}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(10.f, {5.f, 8.f}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 24, 24}); } DEF_TEST_SUITE(CroppedColorFilter, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Regular color filter after empty crop stays empty") .source({0, 0, 16, 16}) .applyCrop(SkIRect::MakeEmpty(), tm, Expect::kEmptyImage) .applyColorFilter(alpha_modulate(0.2f), Expect::kEmptyImage) .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "Transparency-affecting color filter after empty crop creates new image") .source({0, 0, 16, 16}) .applyCrop(SkIRect::MakeEmpty(), tm, Expect::kEmptyImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kNewImage, /*expectedColorFilter=*/nullptr) // CF applied ASAP to new img .run(/*requestedOutput=*/{0, 0, 16, 16}); TestCase(r, "Regular color filter composes with crop") .source({0, 0, 32, 32}) .applyColorFilter(alpha_modulate(0.7f), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop composes with regular color filter") .source({0, 0, 32, 32}) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); // FIXME need to disable the stats tracking for renderExpected() and compare() TestCase(r, "Transparency-affecting color filter restricted by crop") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop composes with transparency-affecting color filter") .source({0, 0, 32, 32}) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); } } DEF_TEST_SUITE(CropBetweenColorFilters, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Crop between regular color filters") .source({0, 0, 32, 32}) .applyColorFilter(alpha_modulate(0.8f), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.4f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); if (tm == SkTileMode::kDecal) { TestCase(r, "Crop between transparency-affecting color filters requires new image") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, SkTileMode::kDecal, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Output-constrained crop between transparency-affecting filters does not") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, SkTileMode::kDecal, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .run(/*requestedOutput=*/{8, 8, 24, 24}); } else { TestCase(r, "Tiling between transparency-affecting color filters defers image") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); } TestCase(r, "Crop between regular and ATB color filters") .source({0, 0, 32, 32}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop between ATB and regular color filters") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kDeferredImage) .applyCrop({8, 8, 24, 24}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); } } DEF_TEST_SUITE(ColorFilterBetweenCrops, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { for (SkTileMode firstTM : kTileModes) { for (SkTileMode secondTM : kTileModes) { Expect newImageIfNotDecalOrDoubleClamp = secondTM != SkTileMode::kDecal && !(secondTM == SkTileMode::kClamp && firstTM == SkTileMode::kClamp) ? Expect::kNewImage : Expect::kDeferredImage; TestCase(r, "Regular color filter between crops") .source({0, 0, 32, 32}) .applyCrop({4, 4, 24, 24}, firstTM, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyCrop({15, 15, 32, 32}, secondTM, newImageIfNotDecalOrDoubleClamp, secondTM == SkTileMode::kDecal ? firstTM : secondTM) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transparency-affecting color filter between crops") .source({0, 0, 32, 32}) .applyCrop({4, 4, 24, 24}, firstTM, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({15, 15, 32, 32}, secondTM, newImageIfNotDecalOrDoubleClamp, secondTM == SkTileMode::kDecal ? firstTM : secondTM) .run(/*requestedOutput=*/{0, 0, 32, 32}); } } } DEF_TEST_SUITE(CroppedTransformedColorFilter, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { TestCase(r, "Transform -> crop -> regular color filter") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transform -> regular color filter -> crop") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop -> transform -> regular color filter") .source({0, 0, 32, 32}) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop -> regular color filter -> transform") .source({0, 0, 32, 32}) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Regular color filter -> transform -> crop") .source({0, 0, 32, 32}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Regular color filter -> crop -> transform") .source({0, 0, 32, 32}) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); } DEF_TEST_SUITE(CroppedTransformedTransparencyAffectingColorFilter, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kNextRelease) { // When the crop is not between the transform and transparency-affecting color filter, // either the order of operations or the bounds propagation means that every action can be // deferred. Below, when the crop is between the two actions, new images are triggered. TestCase(r, "Transform -> transparency-affecting color filter -> crop") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop -> transform -> transparency-affecting color filter") .source({0, 0, 32, 32}) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Crop -> transparency-affecting color filter -> transform") .source({0, 0, 32, 32}) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transparency-affecting color filter -> transform -> crop") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); // Since the crop is between the transform and color filter (or vice versa), transparency // outside the crop is introduced that should not be affected by the color filter were no // new image to be created. TestCase(r, "Transform -> crop -> transparency-affecting color filter") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); TestCase(r, "Transparency-affecting color filter -> crop -> transform") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kNewImage) .run(/*requestedOutput=*/{0, 0, 32, 32}); // However if the output is small enough to fit within the transformed interior, the // transparency is not visible. TestCase(r, "Transform -> crop -> transparency-affecting color filter") .source({0, 0, 32, 32}) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .run(/*requestedOutput=*/{15, 15, 21, 21}); TestCase(r, "Transparency-affecting color filter -> crop -> transform") .source({0, 0, 32, 32}) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) .run(/*requestedOutput=*/{15, 15, 21, 21}); } DEF_TEST_SUITE(BackdropFilterRotated, r, CtsEnforcement::kApiLevel_202404, CtsEnforcement::kNextRelease) { // These values are extracted from a cc_unittest that had a 200x200 image, with a 10-degree // rotated 100x200 layer over the right half of the base image, with a backdrop blur. The // rotation forces SkCanvas to crop and transform the base device's content to be aligned with // the layer space of the blur. The rotation is such that the backdrop image must be clamped // (hence the first crop) and the clamp tiling remains visible in the layer image. However, // floating point precision in the layer bounds analysis was causing FilterResult to think that // the layer decal was also visible so the first crop would be resolved before the transform was // applied. // // While it's expected that the second clamping crop (part of the blur effect), forces the // transform and first clamp to be resolved, we were incorrectly producing two new images // instead of just one. TestCase(r, "Layer decal shouldn't be visible") .source({65, 0, 199, 200}) .applyCrop({65, 0, 199, 200}, SkTileMode::kClamp, Expect::kDeferredImage) .applyTransform(SkMatrix::MakeAll( 0.984808f, 0.173648f, -98.4808f, -0.173648f, 0.984808f, 17.3648f, 0.000000f, 0.000000f, 1.0000f), Expect::kDeferredImage) .applyCrop({0, 0, 100, 200}, SkTileMode::kClamp, Expect::kNewImage) .run(/*requestedOutput=*/{-15, -15, 115, 215}); } // Nearly identity rescales are treated as the identity static constexpr SkSize kNearlyIdentity = {0.999f, 0.999f}; DEF_TEST_SUITE(RescaleWithTileMode, r, CtsEnforcement::kApiLevel_202404, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Identity rescale is a no-op") .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .rescale({1.f, 1.f}, Expect::kDeferredImage) .run(/*requestedOutput=*/{-5, -5, 55, 55}); TestCase(r, "Near identity rescale is a no-op", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .rescale(kNearlyIdentity, Expect::kDeferredImage) .run(/*requestedOutput=*/{-5, -5, 55, 55}); // NOTE: As the scale factor decreases and more decimation steps are required, the testing // allowed tolerances increase greatly. These were chosen as "acceptable" after reviewing // the expected vs. actual images. The results diverge due to differences in the simple // expected decimation and the actual rescale() implementation, as well as how small the // final images become. // // Similarly, the allowed transparent border tolerance must be increased for kDecal tests // because the expected image's content is expanded by a larger and larger factor during its // upscale. TestCase(r, "1-step rescale preserves tile mode", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); const bool periodic = tm == SkTileMode::kRepeat || tm == SkTileMode::kMirror; TestCase(r, "2-step rescale preserves tile mode", /*allowedPercentImageDiff=*/tm == SkTileMode::kDecal ? 5.9f : periodic ? 2.5f : 1.f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 2 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.25f, 0.25f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "2-step rescale with near-identity elision", /*allowedPercentImageDiff=*/periodic ? 17.75f : 41.83f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 8 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.23f, 0.23f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "3-step rescale preserves tile mode", /*allowedPercentImageDiff=*/periodic ? 56.3f : 51.3f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 10 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.155f, 0.155f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); // Non-uniform scales TestCase(r, "Identity X axis, near-identity Y axis is a no-op", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({1.f, kNearlyIdentity.height()}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "Near-identity X axis, identity Y axis is a no-op", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({kNearlyIdentity.width(), 1.f}, Expect::kDeferredImage) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "Identity X axis, 1-step Y axis preserves tile mode", /*allowedPercentImageDiff=*/tm == SkTileMode::kMirror ? 1.32f : 1.f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({1.f, 0.5f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "Near-identity X axis, 1-step Y axis preserves tile mode", /*allowedPercentImageDiff=*/tm == SkTileMode::kMirror ? 1.7f : 1.f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({kNearlyIdentity.width(), 0.5f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "Identity X axis, 2-step Y axis preserves tile mode", /*allowedPercentImageDiff=*/3.1f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 2 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({1.f, 0.25f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "1-step X axis, 2-step Y axis preserves tile mode", /*allowedPercentImageDiff=*/periodic ? 23.2f : 17.7f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 5 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({.55f, 0.27f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "1-step X axis, identity Y axis preserves tile mode", /*allowedPercentImageDiff=*/tm == SkTileMode::kMirror ? 1.2f : 1.f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.5f, 1.f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "1-step X axis, near-identity Y axis preserves tile mode", /*allowedPercentImageDiff=*/tm == SkTileMode::kMirror ? 1.7f : 1.f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.5f, kNearlyIdentity.height()}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "2-step X axis, identity Y axis preserves tile mode", /*allowedPercentImageDiff=*/3.1f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 2 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({0.25f, 1.f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "2-step X axis, 1-step Y axis preserves tile mode", /*allowedPercentImageDiff=*/periodic ? 14.9f : 14.2f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 5 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .rescale({.27f, 0.55f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); // Chained decal tile modes don't create the circumstances of interest. if (tm == SkTileMode::kDecal) { continue; } TestCase(r, "Rescale applies layer bounds", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/1) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyCrop({4, 4, 76, 76}, SkTileMode::kDecal, Expect::kDeferredImage, /*expectedTileMode=*/tm) .rescale({0.5f, 0.5f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); } } DEF_TEST_SUITE(RescaleWithTransform, r, CtsEnforcement::kApiLevel_202404, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Identity rescale defers integer translation") .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Translate(-10.f, -10.f), Expect::kDeferredImage) .rescale({1.f, 1.f}, Expect::kDeferredImage) .run(/*requestedOutput=*/{-15, -15, 45, 45}); TestCase(r, "Identity rescale applies complex transform") .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {16.f, 16.f}), Expect::kDeferredImage) .rescale({1.f, 1.f}, Expect::kNewImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "Near-identity rescale defers integer translation", /*allowedPercentImageDiff=*/kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Translate(-10.f, -10.f), Expect::kDeferredImage) .rescale(kNearlyIdentity, Expect::kDeferredImage) .run(/*requestedOutput=*/{-15, -15, 45, 45}); TestCase(r, "Near-identity rescale applies complex transform") .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(15.f, {25.f, 25.f}), Expect::kDeferredImage) .rescale(kNearlyIdentity, Expect::kNewImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{-5, -5, 55, 55}); TestCase(r, "Identity rescale with deferred scale applies transform in first step") .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Scale(0.4f, 0.4f), Expect::kDeferredImage) .rescale({1.f, 1.f}, Expect::kNewImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{-10, -10, 30, 30}); TestCase(r, "Near-identity rescale with deferred scale applies transform in first step", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Scale(0.4f, 0.4f), Expect::kDeferredImage) .rescale(kNearlyIdentity, Expect::kNewImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{-10, -10, 30, 30}); const bool periodic = tm == SkTileMode::kRepeat || tm == SkTileMode::kMirror; TestCase(r, "1-step rescale applies complex transform in first step", /*allowedPercentImageDiff=*/periodic ? 1.1f : kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {16.f, 16.f}), Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); TestCase(r, "2-step rescale applies complex transform", /*allowedPercentImageDiff=*/periodic ? 10.05f: 3.7f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 4 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::RotateDeg(45.f, {16.f, 16.f}), Expect::kDeferredImage) .rescale({0.25f, 0.25f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); // W/o resolving the deferred transform, the first rescale step could end up with a scale // that's much less than 1/2 and sampling would miss a lot of data. TestCase(r, "Rescale with deferred downscale applies transform before first step", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Scale(0.4f, 0.4f), Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); // But for upscaling, it doesn't contribute to such sampling errors. TestCase(r, "Rescale with deferred upscale applies transform with first step", /*allowedPercentImageDiff=*/2.55f, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 3 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyTransform(SkMatrix::Scale(1.5f, 1.5f), Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) .run(/*requestedOutput=*/{0, 0, 80, 80}); } } DEF_TEST_SUITE(RescaleWithColorFilter, r, CtsEnforcement::kApiLevel_202404, CtsEnforcement::kNextRelease) { for (SkTileMode tm : kTileModes) { TestCase(r, "Identity rescale applies color filter but defers tile mode") .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .rescale({1.f, 1.f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{-5, -5, 55, 55}); TestCase(r, "Near-identity rescale applies color filter but defers tile mode", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({0, 0, 50, 50}) .applyCrop({0, 0, 50, 50}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage) .rescale(kNearlyIdentity, Expect::kNewImage, tm) .run(/*requestedOutput=*/{-5, -5, 55, 55}); TestCase(r, "Rescale applies color filter but defers tile mode", kDefaultMaxAllowedPercentImageDiff, /*transparentCheckBorderTolerance=*/tm == SkTileMode::kDecal ? 1 : 0) .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyColorFilter(alpha_modulate(0.75f), Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, tm) .run(/*requestedOutput=*/{0, 0, 80, 80}); // The color filter (simple and transparency-affecting) should be applied with a 1px // boundary around the rest of the image being rescaled when decal-tiled, so its result is // clamped tiled instead (vs. having to prepare and scale a larger, flood-filled image). SkTileMode expectedTileMode = tm == SkTileMode::kDecal ? SkTileMode::kClamp : tm; TestCase(r, "Rescale applies transparency-affecting color filter but defers tile mode") .source({16, 16, 64, 64}) .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) .rescale({0.5f, 0.5f}, Expect::kNewImage, expectedTileMode) .run(/*requestedOutput=*/{0, 0, 80, 80}); } } DEF_TEST_SUITE(MakeFromImage, r, CtsEnforcement::kApiLevel_202404, CtsEnforcement::kNextRelease) { static constexpr SkISize kSrcSize = {128,128}; static constexpr SkIRect kIdentitySrc = {0,0,128,128}; static constexpr SkIRect kSubsetSrc = {16,16,112,112}; static constexpr SkIRect kOverlappingSrc = {-64, 16, 192, 112}; static constexpr SkIRect kContainingSrc = {-64,-64,192,192}; static constexpr SkIRect kDisjointSrc = {0,-200,128,-1}; // For convenience, most tests will use kIdentitySrc as the dstRect so that the result's // layer bounds can be used to validate the src->dst transform is preserved. static constexpr SkIRect kDstRect = kIdentitySrc; // Sufficiently large to not affect the layer bounds of a FilterResult. static constexpr SkIRect kDesiredOutput = {-400, -400, 400, 400}; sk_sp colorSpace = SkColorSpace::MakeSRGB(); Context ctx{r.refBackend(), Mapping(), LayerSpace(kDesiredOutput), /*source=*/{}, colorSpace.get(), /*stats=*/nullptr}; sk_sp source = r.createSourceImage(kSrcSize, colorSpace); SkASSERT(source->subset() == kIdentitySrc); sk_sp sourceImage = source->asImage(); auto makeImage = [&](SkIRect src, SkIRect dst) { ParameterSpace dstRect{SkRect::Make(dst)}; return FilterResult::MakeFromImage(ctx, sourceImage, SkRect::Make(src), dstRect, {}); }; // Failure cases should return an empty FilterResult REPORTER_ASSERT(r, !SkToBool(makeImage(kIdentitySrc, SkIRect::MakeEmpty())), "Empty dst rect returns empty FilterResult"); REPORTER_ASSERT(r, !SkToBool(makeImage(SkIRect::MakeEmpty(), kDstRect)), "Empty src rect returns empty FilterResult"); REPORTER_ASSERT(r, !SkToBool(makeImage(kDisjointSrc, kDstRect)), "Disjoint src rect returns empty FilterREsult"); auto testSuccess = [&](SkIRect src, SkIRect expectedImageSubset, SkIRect expectedLayerBounds, const char* label) { auto result = makeImage(src, kDstRect); REPORTER_ASSERT(r, SkToBool(result), "Image should not be empty: %s", label); REPORTER_ASSERT(r, result.image()->subset() == expectedImageSubset, "Result subset is incorrect: %s", label); REPORTER_ASSERT(r, SkIRect(result.layerBounds()) == expectedLayerBounds, "Result layer bounds are incorrect: %s", label); }; testSuccess(kIdentitySrc, kIdentitySrc, kDstRect, "Identity src->dst preserves original image bounds"); testSuccess(kSubsetSrc, kSubsetSrc, kDstRect, "Contained src rect is preserved, stretched to original dst bounds"); testSuccess(kOverlappingSrc, {0,16,128,112}, {32,0,96,128}, "Overlapping src rect is clipped and dst is scaled on clipped axis"); testSuccess(kContainingSrc, kIdentitySrc, {32,32,96,96}, "Containing src rect is clipped and dst is scaled on both axes"); } } // anonymous namespace