| /* | |
| * 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/SkAssert.h" | |
| #include "include/private/SkDebug.h" | |
| #include "include/private/SkTArray.h" | |
| #include "include/private/SkTo.h" | |
| #include "src/core/SkColorData.h" | |
| #include "src/core/SkDevice.h" | |
| #include "src/core/SkImageFilterTypes.h" | |
| #include "src/core/SkMatrixPriv.h" | |
| #include "src/core/SkRectPriv.h" | |
| #include "src/core/SkSpecialImage.h" | |
| #include "src/effects/colorfilters/SkColorFilterBase.h" | |
| #include "src/gpu/ganesh/image/GrImageUtils.h" | |
| #include "tests/CtsEnforcement.h" | |
| #include "tests/Test.h" | |
| #include "tools/EncodeUtils.h" | |
| #include "tools/gpu/ContextType.h" | |
| #include <array> | |
| #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, /*allowOverscaling=*/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 {}; | |
| } | |
| } | |
| enum class ShaderSampleMode { | |
| kFast, | |
| kShaderClamp, | |
| kShaderTile | |
| }; | |
| static ShaderSampleMode GetExpectedShaderSampleMode(const skif::Context& ctx, | |
| const skif::FilterResult& image, | |
| bool actionSupportsDirectDrawing) { | |
| if (!image) { | |
| return ShaderSampleMode::kFast; | |
| } | |
| auto analysis = image.analyzeBounds(ctx.desiredOutput()); | |
| bool mustFillDecal = image.tileMode() == SkTileMode::kDecal && | |
| (analysis & BoundsAnalysis::kDstBoundsNotCovered) && | |
| !actionSupportsDirectDrawing; | |
| if ((analysis & BoundsAnalysis::kHasLayerFillingEffect) || mustFillDecal) { | |
| // The image won't be drawn directly so some form of shader is needed. The faster clamp | |
| // can be used when clamping explicitly or decal-with-transparent-padding. | |
| if (image.tileMode() == SkTileMode::kClamp || | |
| (image.tileMode() == SkTileMode::kDecal && | |
| image.fBoundary == FilterResult::PixelBoundary::kTransparent)) { | |
| return ShaderSampleMode::kShaderClamp; | |
| } else { | |
| // These cases should be covered by the more expensive shader tiling. | |
| return ShaderSampleMode::kShaderTile; | |
| } | |
| } | |
| // If we got here, it will be drawn directly but a clamp can be needed if the data outside | |
| // the image is unknown and sampling might pull those values in accidentally. | |
| if (image.fBoundary == FilterResult::PixelBoundary::kUnknown) { | |
| return ShaderSampleMode::kShaderClamp; | |
| } else { | |
| return ShaderSampleMode::kFast; | |
| } | |
| } | |
| }; | |
| namespace { | |
| // Parameters controlling the fuzziness matching of expected and actual images. | |
| // NOTE: When image fuzzy diffing fails it will print the expected image, the actual image, and | |
| // an "error" image where all bad pixels have been set to red. You can select all three base64 | |
| // encoded PNGs, copy them, and run the following command to view in detail: | |
| // xsel -o | viewer --file stdin | |
| static constexpr float kDefaultRGBTolerance = 8.f / 255.f; | |
| static constexpr float kAATolerance = 2.f / 255.f; | |
| static constexpr float kDefaultMaxAllowedPercentImageDiff = 1.f; | |
| static constexpr std::array<std::array<const float, 3>, 3> kFuzzyKernel = {{ | |
| {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}; | |
| } | |
| } | |
| FilterResultTestAccess::ShaderSampleMode expectedSampleMode(const Context& ctx, | |
| const FilterResult& source) const { | |
| bool actionSupportsDirectDrawing = true; | |
| if (std::holds_alternative<RescaleParams>(fAction)) { | |
| // rescale() normally does not draw directly; the exception is if the source image has | |
| // a scale factor that requires a pre-resolve. If that happens 'source' is not really | |
| // the source of the rescale steps, and `source` can be drawn directly by the resolve. | |
| auto scales = FilterResultTestAccess::DeferredScaleFactors(source); | |
| if (!scales || scales->first > 0.5f) { | |
| actionSupportsDirectDrawing = false; // no pre-resolve | |
| } | |
| } | |
| return FilterResultTestAccess::GetExpectedShaderSampleMode( | |
| ctx, source, actionSupportsDirectDrawing); | |
| } | |
| LayerSpace<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, | |
| float allowedRGBTolerance, | |
| int transparentCheckBorderPadding) { | |
| 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); | |
| auto compare = [&](ResolveMethod m) { | |
| return this->compareImages(ctx, expectedBM, expectedOrigin, actual, m, | |
| allowedPercentImageDiff, | |
| allowedRGBTolerance, | |
| transparentCheckBorderPadding); | |
| }; | |
| // 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 compare(ResolveMethod::kImageAndOffset) && | |
| compare(ResolveMethod::kDrawToCanvas) && | |
| compare(ResolveMethod::kShader) && | |
| compare(ResolveMethod::kClippedShader); | |
| } | |
| 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, | |
| /*allowedRGBTolerance=*/kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/0); | |
| } | |
| sk_sp<SkSpecialImage> createSourceImage(SkISize size, sk_sp<SkColorSpace> colorSpace) { | |
| sk_sp<SkDevice> sourceSurface = fBackend->makeDevice(size, std::move(colorSpace)); | |
| static constexpr auto colors = std::to_array<SkColor>({ | |
| 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, float allowedRGBTolerance, | |
| int transparentCheckBorderPadding) { | |
| 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, allowedRGBTolerance, | |
| transparentCheckBorderPadding, &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, | |
| float allowedRGBTolerance, | |
| int transparentCheckBorderPadding, | |
| 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; | |
| float errorDelta = 0.f; | |
| 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) { | |
| const float delta = this->approxColorDelta( | |
| this->boxFilter(actual, x, y), | |
| this->boxFilter(expected, ep.fX, ep.fY)); | |
| if (delta > allowedRGBTolerance) { | |
| errorDelta += delta; | |
| 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 %%), " | |
| "average delta %f (vs. %f allowed)", | |
| errorCount, totalCount, percentError, allowedPercentImageDiff, | |
| errorDelta / errorCount, allowedRGBTolerance); | |
| 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(transparentCheckBorderPadding, | |
| transparentCheckBorderPadding); | |
| } | |
| 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); | |
| const float delta = this->approxColorDelta(expected.getColor4f(x, y), | |
| SkColors::kTransparent); | |
| if (delta > (onEdge ? kAATolerance : 0.f)) { | |
| badPixels->push_back({x, y}); | |
| badTransparencyCount++; | |
| } | |
| } | |
| } | |
| REPORTER_ASSERT(fReporter, badTransparencyCount == 0, "Unexpected non-transparent pixels"); | |
| return badTransparencyCount == 0; | |
| } | |
| float approxColorDelta(const SkColor4f& a, const SkColor4f& b) 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); | |
| return sqrt((2.f + r)*dr*dr + 4.f*dg*dg + (2.f + (1.f - r))*db*db); | |
| } | |
| 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, 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, | |
| float allowedRGBTolerance=kDefaultRGBTolerance, | |
| int transparentCheckBorderPadding=0) | |
| : fRunner(runner) | |
| , fName(name) | |
| , fAllowedPercentImageDiff(allowedPercentImageDiff) | |
| , fAllowedRGBTolerance(allowedRGBTolerance) | |
| , fTransparentCheckBorderPadding(transparentCheckBorderPadding) | |
| , 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{SkM44()}, | |
| 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); | |
| using ShaderSampleMode = FilterResultTestAccess::ShaderSampleMode; | |
| auto expectedSampleMode = fActions[i].expectedSampleMode(ctx, source); | |
| REPORTER_ASSERT(fRunner, stats.fNumShaderBasedTilingDraws == 0 || | |
| expectedSampleMode == ShaderSampleMode::kShaderTile); | |
| REPORTER_ASSERT(fRunner, stats.fNumShaderClampedDraws == 0 || | |
| expectedSampleMode == ShaderSampleMode::kShaderClamp); | |
| // Validate layer bounds and sampling when we expect a new or deferred image | |
| if (output.image()) { | |
| auto actualBounds = output.layerBounds(); | |
| // A deferred action doesn't have to crop its layer bounds to the desired output to | |
| // preserve accuracy of later bounds analysis. New images however should restrict | |
| // themselves to the desired output to minimize memory of the surface. The exception | |
| // is a new image for applyTransform() because the new transform is deferred to the | |
| // resolved image, which can make its layer bounds larger than the desired output. | |
| if (correctedExpectation == Expect::kDeferredImage || | |
| !FilterResultTestAccess::IsIntegerTransform(output)) { | |
| // Skip the check if the desiredOutputs's SkIRect reports empty. | |
| // LayerSpace<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, | |
| fAllowedRGBTolerance, | |
| fTransparentCheckBorderPadding)) { | |
| // 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; | |
| float fAllowedRGBTolerance; | |
| int fTransparentCheckBorderPadding; | |
| // 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/40045510) - 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, ctsEnforcement) // 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, ctsEnforcement) // 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) | |
| static std::string to_string(SkTileMode tm) { | |
| switch (tm) { | |
| case SkTileMode::kClamp: return "clamp"; | |
| case SkTileMode::kRepeat: return "repeat"; | |
| case SkTileMode::kMirror: return "mirror"; | |
| case SkTileMode::kDecal: return "decal"; | |
| default: return "invalid"; | |
| } | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Empty input/output tests | |
| DEF_TEST_SUITE(EmptySource, r, CtsEnforcement::kApiLevel_T, CtsEnforcement::kApiLevel_202604) { | |
| // 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) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| // 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) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| // 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) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| // 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) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : {SkTileMode::kRepeat, SkTileMode::kMirror}) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| // 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| // 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| // Test interactions of non-decal tile modes and transforms | |
| for (SkTileMode tm : kTileModes) { | |
| if (tm == SkTileMode::kDecal) { | |
| continue; | |
| } | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| // 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| 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::kApiLevel_202604) { | |
| // When the crop is not between the transform and transparency-affecting color filter, | |
| // either the order of operations or the bounds propagation means that every action can be | |
| // deferred. Below, when the crop is between the two actions, new images are triggered. | |
| TestCase(r, "Transform -> transparency-affecting color filter -> crop") | |
| .source({0, 0, 32, 32}) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| TestCase(r, "Crop -> transform -> transparency-affecting color filter") | |
| .source({0, 0, 32, 32}) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| TestCase(r, "Crop -> transparency-affecting color filter -> transform") | |
| .source({0, 0, 32, 32}) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| TestCase(r, "Transparency-affecting color filter -> transform -> crop") | |
| .source({0, 0, 32, 32}) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| // Since the crop is between the transform and color filter (or vice versa), transparency | |
| // outside the crop is introduced that should not be affected by the color filter were no | |
| // new image to be created. | |
| TestCase(r, "Transform -> crop -> transparency-affecting color filter") | |
| .source({0, 0, 32, 32}) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kNewImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| TestCase(r, "Transparency-affecting color filter -> crop -> transform") | |
| .source({0, 0, 32, 32}) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kNewImage) | |
| .run(/*requestedOutput=*/{0, 0, 32, 32}); | |
| // However if the output is small enough to fit within the transformed interior, the | |
| // transparency is not visible. | |
| TestCase(r, "Transform -> crop -> transparency-affecting color filter") | |
| .source({0, 0, 32, 32}) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{15, 15, 21, 21}); | |
| TestCase(r, "Transparency-affecting color filter -> crop -> transform") | |
| .source({0, 0, 32, 32}) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .applyCrop({2, 2, 30, 30}, Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::RotateDeg(30.f, {16, 16}), Expect::kDeferredImage) | |
| .run(/*requestedOutput=*/{15, 15, 21, 21}); | |
| } | |
| DEF_TEST_SUITE(BackdropFilterRotated, | |
| r, | |
| CtsEnforcement::kApiLevel_202404, | |
| CtsEnforcement::kApiLevel_202604) { | |
| // These values are extracted from a cc_unittest that had a 200x200 image, with a 10-degree | |
| // rotated 100x200 layer over the right half of the base image, with a backdrop blur. The | |
| // rotation forces SkCanvas to crop and transform the base device's content to be aligned with | |
| // the layer space of the blur. The rotation is such that the backdrop image must be clamped | |
| // (hence the first crop) and the clamp tiling remains visible in the layer image. However, | |
| // floating point precision in the layer bounds analysis was causing FilterResult to think that | |
| // the layer decal was also visible so the first crop would be resolved before the transform was | |
| // applied. | |
| // | |
| // While it's expected that the second clamping crop (part of the blur effect), forces the | |
| // transform and first clamp to be resolved, we were incorrectly producing two new images | |
| // instead of just one. | |
| TestCase(r, "Layer decal shouldn't be visible") | |
| .source({65, 0, 199, 200}) | |
| .applyCrop({65, 0, 199, 200}, SkTileMode::kClamp, Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::MakeAll( 0.984808f, 0.173648f, -98.4808f, | |
| -0.173648f, 0.984808f, 17.3648f, | |
| 0.000000f, 0.000000f, 1.0000f), | |
| Expect::kDeferredImage) | |
| .applyCrop({0, 0, 100, 200}, SkTileMode::kClamp, Expect::kNewImage) | |
| .run(/*requestedOutput=*/{-15, -15, 115, 215}); | |
| } | |
| // Nearly identity rescales are treated as the identity | |
| static constexpr SkSize kNearlyIdentity = {0.999f, 0.999f}; | |
| DEF_TEST_SUITE(RescaleWithTileMode, | |
| r, | |
| CtsEnforcement::kApiLevel_202404, | |
| CtsEnforcement::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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 ? 13.f : 30.f, | |
| kDefaultRGBTolerance + 2.f / 255.f, | |
| /*transparentCheckBorderPadding=*/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=*/28.f, | |
| kDefaultRGBTolerance + 4.f / 255.f, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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 ? 16.5f : 14.f, | |
| kDefaultRGBTolerance + 2.f / 255.f, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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 ? 13.f : 10.5f, | |
| kDefaultRGBTolerance + 2.f/255.f, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/1) | |
| .source({16, 16, 64, 64}) | |
| .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) | |
| .applyCrop({4, 4, 76, 76}, SkTileMode::kDecal, Expect::kDeferredImage, | |
| /*expectedTileMode=*/tm) | |
| .rescale({0.5f, 0.5f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) | |
| .run(/*requestedOutput=*/{0, 0, 80, 80}); | |
| } | |
| } | |
| DEF_TEST_SUITE(RescaleWithTransform, | |
| r, | |
| CtsEnforcement::kApiLevel_202404, | |
| CtsEnforcement::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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", | |
| kDefaultMaxAllowedPercentImageDiff, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/tm == SkTileMode::kDecal ? 3 : 0) | |
| .source({16, 16, 64, 64}) | |
| .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) | |
| .applyTransform(SkMatrix::Scale(1.5f, 1.5f), Expect::kDeferredImage) | |
| .rescale({0.5f, 0.5f}, Expect::kNewImage, /*expectedTileMode=*/SkTileMode::kDecal) | |
| .run(/*requestedOutput=*/{0, 0, 80, 80}); | |
| } | |
| } | |
| DEF_TEST_SUITE(RescaleWithColorFilter, | |
| r, | |
| CtsEnforcement::kApiLevel_202404, | |
| CtsEnforcement::kApiLevel_202604) { | |
| for (SkTileMode tm : kTileModes) { | |
| skiatest::ReporterContext tileMode(r, to_string(tm)); | |
| 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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/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, | |
| kDefaultRGBTolerance, | |
| /*transparentCheckBorderPadding=*/tm == SkTileMode::kDecal ? 1 : 0) | |
| .source({16, 16, 64, 64}) | |
| .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) | |
| .applyColorFilter(alpha_modulate(0.75f), Expect::kDeferredImage) | |
| .rescale({0.5f, 0.5f}, Expect::kNewImage, tm) | |
| .run(/*requestedOutput=*/{0, 0, 80, 80}); | |
| // The color filter (simple and transparency-affecting) should be applied with a 1px | |
| // boundary around the rest of the image being rescaled when decal-tiled, so its result is | |
| // clamped tiled instead (vs. having to prepare and scale a larger, flood-filled image). | |
| SkTileMode expectedTileMode = tm == SkTileMode::kDecal ? SkTileMode::kClamp : tm; | |
| TestCase(r, "Rescale applies transparency-affecting color filter but defers tile mode") | |
| .source({16, 16, 64, 64}) | |
| .applyCrop({16, 16, 64, 64}, tm, Expect::kDeferredImage) | |
| .applyColorFilter(affect_transparent(SkColors::kGreen), Expect::kDeferredImage) | |
| .rescale({0.5f, 0.5f}, Expect::kNewImage, expectedTileMode) | |
| .run(/*requestedOutput=*/{0, 0, 80, 80}); | |
| } | |
| } | |
| DEF_TEST_SUITE(MakeFromImage, | |
| r, | |
| CtsEnforcement::kApiLevel_202404, | |
| CtsEnforcement::kApiLevel_202604) { | |
| 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 |