| /* |
| * 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/SkColorData.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/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 <cmath> |
| #include <initializer_list> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| |
| #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<SkShader> AsShader(const skif::Context& ctx, |
| const skif::FilterResult& image, |
| const skif::LayerSpace<SkIRect>& sampleBounds) { |
| return image.asShader(ctx, FilterResult::kDefaultSampling, |
| FilterResult::ShaderFlags::kNone, sampleBounds); |
| } |
| |
| static sk_sp<SkShader> 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<SkSize> 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<std::pair<float, float>> DeferredScaleFactors( |
| const skif::FilterResult& image) { |
| float scaleFactors[2]; |
| if (SkMatrix(image.fTransform).getMinMaxScales(scaleFactors)) { |
| return {{scaleFactors[0], scaleFactors[1]}}; |
| } else { |
| return {}; |
| } |
| } |
| |
| static bool IsShaderTilingExpected(const skif::Context& ctx, |
| const skif::FilterResult& image) { |
| if (image.tileMode() == SkTileMode::kClamp) { |
| return false; |
| } |
| if (image.tileMode() == SkTileMode::kDecal && |
| image.fBoundary == FilterResult::PixelBoundary::kTransparent) { |
| return false; |
| } |
| auto analysis = image.analyzeBounds(ctx.desiredOutput()); |
| if (!(analysis & BoundsAnalysis::kHasLayerFillingEffect)) { |
| return false; |
| } |
| |
| // If we got here, it's either a mirror/repeat tile mode that's visible so a shader has to |
| // be used if the image isn't HW tileable; OR it's a decal tile mode without transparent |
| // padding that can't be drawn directly (in this case hasLayerFillingEffect implies a |
| // color filter that has to evaluate the decal'ed sampling). |
| return true; |
| } |
| |
| static bool IsShaderClampingExpected(const skif::Context& ctx, |
| const skif::FilterResult& image) { |
| auto analysis = image.analyzeBounds(ctx.desiredOutput()); |
| if (analysis & BoundsAnalysis::kHasLayerFillingEffect) { |
| // 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 true; |
| } else { |
| // These cases should be covered by the more expensive shader tiling. |
| SkASSERT(IsShaderTilingExpected(ctx, image)); |
| return false; |
| } |
| } |
| // 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. |
| return image.fBoundary == FilterResult::PixelBoundary::kUnknown; |
| } |
| }; |
| |
| 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<SkData> actualData = actual->serialize(); |
| sk_sp<SkData> 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<SkMatrix> fMatrix; |
| SkSamplingOptions fSampling; |
| }; |
| struct CropParams { |
| LayerSpace<SkIRect> fRect; |
| SkTileMode fTileMode; |
| // Sometimes the expected bounds due to cropping and tiling are too hard to automate with |
| // simple test code. |
| std::optional<LayerSpace<SkIRect>> fExpectedBounds; |
| }; |
| struct RescaleParams { |
| LayerSpace<SkSize> fScale; |
| }; |
| |
| public: |
| ApplyAction(const SkMatrix& transform, |
| const SkSamplingOptions& sampling, |
| Expect expectation, |
| const SkSamplingOptions& expectedSampling, |
| SkTileMode expectedTileMode, |
| sk_sp<SkColorFilter> expectedColorFilter) |
| : fAction{TransformParams{LayerSpace<SkMatrix>(transform), sampling}} |
| , fExpectation(expectation) |
| , fExpectedSampling(expectedSampling) |
| , fExpectedTileMode(expectedTileMode) |
| , fExpectedColorFilter(std::move(expectedColorFilter)) {} |
| |
| ApplyAction(const SkIRect& cropRect, |
| SkTileMode tileMode, |
| std::optional<LayerSpace<SkIRect>> expectedBounds, |
| Expect expectation, |
| const SkSamplingOptions& expectedSampling, |
| SkTileMode expectedTileMode, |
| sk_sp<SkColorFilter> expectedColorFilter) |
| : fAction{CropParams{LayerSpace<SkIRect>(cropRect), tileMode, expectedBounds}} |
| , fExpectation(expectation) |
| , fExpectedSampling(expectedSampling) |
| , fExpectedTileMode(expectedTileMode) |
| , fExpectedColorFilter(std::move(expectedColorFilter)) {} |
| |
| ApplyAction(sk_sp<SkColorFilter> colorFilter, |
| Expect expectation, |
| const SkSamplingOptions& expectedSampling, |
| SkTileMode expectedTileMode, |
| sk_sp<SkColorFilter> expectedColorFilter) |
| : fAction(std::move(colorFilter)) |
| , fExpectation(expectation) |
| , fExpectedSampling(expectedSampling) |
| , fExpectedTileMode(expectedTileMode) |
| , fExpectedColorFilter(std::move(expectedColorFilter)) {} |
| |
| ApplyAction(LayerSpace<SkSize> scale, |
| Expect expectation, |
| const SkSamplingOptions& expectedSampling, |
| SkTileMode expectedTileMode, |
| sk_sp<SkColorFilter> 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<SkIRect> requiredInput(const LayerSpace<SkIRect>& desiredOutput) const { |
| if (auto* t = std::get_if<TransformParams>(&fAction)) { |
| LayerSpace<SkIRect> out; |
| return t->fMatrix.inverseMapRect(desiredOutput, &out) |
| ? out : LayerSpace<SkIRect>::Empty(); |
| } else if (auto* c = std::get_if<CropParams>(&fAction)) { |
| LayerSpace<SkIRect> intersection = c->fRect; |
| if (c->fTileMode == SkTileMode::kDecal && !intersection.intersect(desiredOutput)) { |
| intersection = LayerSpace<SkIRect>::Empty(); |
| } |
| return intersection; |
| } else if (std::holds_alternative<sk_sp<SkColorFilter>>(fAction) || |
| std::holds_alternative<RescaleParams>(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<TransformParams>(&fAction)) { |
| return in.applyTransform(ctx, t->fMatrix, t->fSampling); |
| } else if (auto* c = std::get_if<CropParams>(&fAction)) { |
| return in.applyCrop(ctx, c->fRect, c->fTileMode); |
| } else if (auto* cf = std::get_if<sk_sp<SkColorFilter>>(&fAction)) { |
| return in.applyColorFilter(ctx, *cf); |
| } else if (auto* s = std::get_if<RescaleParams>(&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<int> expectedOffscreenSurfaces(const FilterResult& source) const { |
| if (fExpectation != Expect::kNewImage) { |
| return {0}; |
| } |
| if (auto* s = std::get_if<RescaleParams>(&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}; |
| } |
| } |
| |
| LayerSpace<SkIRect> expectedBounds(const LayerSpace<SkIRect>& inputBounds) const { |
| // This assumes anything outside 'inputBounds' is transparent black. |
| if (auto* t = std::get_if<TransformParams>(&fAction)) { |
| if (inputBounds.isEmpty()) { |
| return LayerSpace<SkIRect>::Empty(); |
| } |
| return t->fMatrix.mapRect(inputBounds); |
| } else if (auto* c = std::get_if<CropParams>(&fAction)) { |
| if (c->fExpectedBounds) { |
| return *c->fExpectedBounds; |
| } |
| |
| LayerSpace<SkIRect> intersection = c->fRect; |
| if (!intersection.intersect(inputBounds)) { |
| return LayerSpace<SkIRect>::Empty(); |
| } |
| return c->fTileMode == SkTileMode::kDecal |
| ? intersection : LayerSpace<SkIRect>(SkRectPriv::MakeILarge()); |
| } else if (auto* cf = std::get_if<sk_sp<SkColorFilter>>(&fAction)) { |
| if (as_CFB(*cf)->affectsTransparentBlack()) { |
| // Fills out infinitely |
| return LayerSpace<SkIRect>(SkRectPriv::MakeILarge()); |
| } else { |
| return inputBounds; |
| } |
| } else if (std::holds_alternative<RescaleParams>(fAction)) { |
| return inputBounds; |
| } |
| SkUNREACHABLE; |
| } |
| |
| sk_sp<SkSpecialImage> renderExpectedImage(const Context& ctx, |
| sk_sp<SkSpecialImage> source, |
| LayerSpace<SkIPoint> origin, |
| const LayerSpace<SkIRect>& 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<SkIRect> sourceBounds{ |
| SkIRect::MakeXYWH(origin.x(), origin.y(), source->width(), source->height())}; |
| LayerSpace<SkIRect> 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<TransformParams>(&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<CropParams>(&fAction)) { |
| LayerSpace<SkIRect> 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<sk_sp<SkColorFilter>>(&fAction)) { |
| paint.setColorFilter(*cf); |
| } else if (auto* s = std::get_if<RescaleParams>(&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<SkIPoint>({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<TransformParams, // for applyTransform() |
| CropParams, // for applyCrop() |
| sk_sp<SkColorFilter>,// for applyColorFilter() |
| RescaleParams // for rescale() |
| > fAction; |
| |
| // Expectation |
| Expect fExpectation; |
| SkSamplingOptions fExpectedSampling; |
| SkTileMode fExpectedTileMode; |
| sk_sp<SkColorFilter> 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<sk_sp<SkSpecialImage>, SkIPoint> resolve(const Context& ctx, |
| const FilterResult& image) const { |
| if (fMethod == Method::kImageAndOffset) { |
| SkIPoint origin; |
| sk_sp<SkSpecialImage> 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<SkShader> 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<SkIPoint>(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<skif::Backend> 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<SkSpecialImage> createSourceImage(SkISize size, sk_sp<SkColorSpace> colorSpace) { |
| sk_sp<SkDevice> 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<SkIPoint> 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<SkIPoint>* 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<SkImage> 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<SkIPoint>& 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<skif::Backend> 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<SkIRect>::Empty()) |
| , fDesiredOutput(LayerSpace<SkIRect>::Empty()) {} |
| |
| TestCase& source(const SkIRect& bounds) { |
| fSourceBounds = LayerSpace<SkIRect>(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<SkTileMode> expectedTileMode = {}, |
| std::optional<SkIRect> expectedBounds = {}) { |
| // Fill-in automated expectations, which is to equal 'tileMode' when not overridden. |
| if (!expectedTileMode) { |
| expectedTileMode = tileMode; |
| } |
| std::optional<LayerSpace<SkIRect>> expectedLayerBounds; |
| if (expectedBounds) { |
| expectedLayerBounds = LayerSpace<SkIRect>(*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<SkSamplingOptions> 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<SkColorFilter> colorFilter, |
| Expect expectation, |
| std::optional<sk_sp<SkColorFilter>> 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<SkTileMode> expectedTileMode = {}) { |
| SkASSERT(!fActions.empty()); |
| if (!expectedTileMode) { |
| expectedTileMode = this->getDefaultExpectedTileMode(expectation, |
| /*cfAffectsTransparency=*/false); |
| } |
| fActions.emplace_back(skif::LayerSpace<SkSize>(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<SkIRect>(requestedOutput); |
| std::vector<LayerSpace<SkIRect>> 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<SkISize>({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<SkColorSpace> colorSpace = SkColorSpace::MakeSRGB(); |
| FilterResult source; |
| if (!fSourceBounds.isEmpty()) { |
| source = FilterResult(fRunner.createSourceImage(SkISize(fSourceBounds.size()), |
| colorSpace), |
| fSourceBounds.topLeft()); |
| } |
| |
| Context baseContext{fRunner.refBackend(), |
| skif::Mapping{SkMatrix::I()}, |
| skif::LayerSpace<SkIRect>::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<SkSpecialImage> expectedImage = source.refImage(); |
| LayerSpace<SkIPoint> expectedOrigin = source.layerBounds().topLeft(); |
| // The expected image can't ever be null, so we produce a transparent black image instead. |
| if (!expectedImage) { |
| sk_sp<SkDevice> expectedSurface = fRunner.backend()->makeDevice({1, 1}, colorSpace); |
| clear_device(expectedSurface.get()); |
| expectedImage = expectedSurface->snapSpecial(SkIRect::MakeWH(1, 1)); |
| expectedOrigin = LayerSpace<SkIPoint>({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<SkIRect> 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<SkIRect>::Empty(); |
| correctedExpectation = Expect::kEmptyImage; |
| } |
| |
| std::vector<int> 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); |
| REPORTER_ASSERT(fRunner, stats.fNumShaderBasedTilingDraws == 0 || |
| FilterResultTestAccess::IsShaderTilingExpected(ctx, source)); |
| REPORTER_ASSERT(fRunner, stats.fNumShaderClampedDraws == 0 || |
| FilterResultTestAccess::IsShaderClampingExpected(ctx, source)); |
| |
| // 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<SkIRect> 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<SkColorFilter> 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<SkIRect> fSourceBounds; |
| |
| // The intended area to fill with the result, controlled by outside factors (e.g. clip bounds) |
| LayerSpace<SkIRect> fDesiredOutput; |
| |
| std::vector<ApplyAction> fActions; |
| }; |
| |
| // ---------------------------------------------------------------------------- |
| // Utilities to create color filters for the unit tests |
| |
| sk_sp<SkColorFilter> 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<SkColorFilter> 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> 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_V, 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_V, 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.22f, |
| /*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 : 13.61f, |
| /*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_V, 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_V, 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_V, 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<SkColorSpace> colorSpace = SkColorSpace::MakeSRGB(); |
| Context ctx{r.refBackend(), |
| Mapping(), |
| LayerSpace<SkIRect>(kDesiredOutput), |
| /*source=*/{}, |
| colorSpace.get(), |
| /*stats=*/nullptr}; |
| |
| sk_sp<SkSpecialImage> source = r.createSourceImage(kSrcSize, colorSpace); |
| SkASSERT(source->subset() == kIdentitySrc); |
| sk_sp<SkImage> sourceImage = source->asImage(); |
| |
| |
| auto makeImage = [&](SkIRect src, SkIRect dst) { |
| ParameterSpace<SkRect> 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 |