| /* |
| * Copyright 2024 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "src/gpu/graphite/geom/AnalyticBlurMask.h" |
| |
| #include "include/core/SkBitmap.h" |
| #include "include/core/SkBlendMode.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkImageFilter.h" |
| #include "include/core/SkImageInfo.h" |
| #include "include/core/SkMatrix.h" |
| #include "include/core/SkPaint.h" |
| #include "include/core/SkRRect.h" |
| #include "include/core/SkRect.h" |
| #include "include/core/SkScalar.h" |
| #include "include/core/SkSize.h" |
| #include "include/effects/SkImageFilters.h" |
| #include "include/gpu/GpuTypes.h" |
| #include "include/gpu/graphite/Recorder.h" |
| #include "include/private/base/SkAlign.h" |
| #include "include/private/base/SkAssert.h" |
| #include "include/private/base/SkFloatingPoint.h" |
| #include "include/private/base/SkMacros.h" |
| #include "include/private/base/SkPoint_impl.h" |
| #include "src/base/SkFloatBits.h" |
| #include "src/core/SkRRectPriv.h" |
| #include "src/gpu/BlurUtils.h" |
| #include "src/gpu/ResourceKey.h" |
| #include "src/gpu/SkBackingFit.h" |
| #include "src/gpu/graphite/Caps.h" |
| #include "src/gpu/graphite/Image_Graphite.h" // IWYU pragma: keep |
| #include "src/gpu/graphite/ProxyCache.h" |
| #include "src/gpu/graphite/RecorderPriv.h" |
| #include "src/gpu/graphite/Surface_Graphite.h" |
| #include "src/gpu/graphite/geom/Transform.h" |
| #include "src/sksl/SkSLUtil.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdint> |
| #include <cstring> |
| |
| namespace skgpu::graphite { |
| |
| namespace { |
| |
| std::optional<Rect> outset_bounds(const SkMatrix& localToDevice, |
| float devSigma, |
| const SkRect& srcRect) { |
| float outsetX = 3.0f * devSigma; |
| float outsetY = 3.0f * devSigma; |
| if (localToDevice.isScaleTranslate()) { |
| outsetX /= std::fabs(localToDevice.getScaleX()); |
| outsetY /= std::fabs(localToDevice.getScaleY()); |
| } else { |
| SkSize scale; |
| if (!localToDevice.decomposeScale(&scale, nullptr)) { |
| return std::nullopt; |
| } |
| outsetX /= scale.width(); |
| outsetY /= scale.height(); |
| } |
| return srcRect.makeOutset(outsetX, outsetY); |
| } |
| |
| } // anonymous namespace |
| |
| std::optional<AnalyticBlurMask> AnalyticBlurMask::Make(Recorder* recorder, |
| const Transform& localToDeviceTransform, |
| float deviceSigma, |
| const SkRRect& srcRRect) { |
| // TODO: Implement SkMatrix functionality used below for Transform. |
| SkMatrix localToDevice = localToDeviceTransform; |
| |
| if (srcRRect.isRect() && localToDevice.preservesRightAngles()) { |
| return MakeRect(recorder, localToDevice, deviceSigma, srcRRect.rect()); |
| } |
| |
| const auto devRRect = srcRRect.transform(localToDevice); |
| if (devRRect.has_value() && SkRRectPriv::IsCircle(*devRRect)) { |
| return MakeCircle(recorder, localToDevice, deviceSigma, srcRRect.rect(), devRRect->rect()); |
| } |
| |
| // A local-space circle transformed by a rotation matrix will fail SkRRect::transform since it |
| // only supports scale + translate matrices, but is still a valid circle that can be blurred. |
| if (SkRRectPriv::IsCircle(srcRRect) && localToDevice.isSimilarity()) { |
| const SkRect srcRect = srcRRect.rect(); |
| const SkPoint devCenter = localToDevice.mapPoint(srcRect.center()); |
| const float devRadius = localToDevice.mapVector(0.0f, srcRect.width() / 2.0f).length(); |
| const SkRect devRect = {devCenter.x() - devRadius, |
| devCenter.y() - devRadius, |
| devCenter.x() + devRadius, |
| devCenter.y() + devRadius}; |
| return MakeCircle(recorder, localToDevice, deviceSigma, srcRect, devRect); |
| } |
| |
| if (devRRect.has_value() && SkRRectPriv::IsSimpleCircular(*devRRect) && |
| localToDevice.isScaleTranslate()) { |
| return MakeRRect(recorder, localToDevice, deviceSigma, srcRRect, *devRRect); |
| } |
| |
| return std::nullopt; |
| } |
| |
| std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRect(Recorder* recorder, |
| const SkMatrix& localToDevice, |
| float devSigma, |
| const SkRect& srcRect) { |
| SkASSERT(srcRect.isSorted()); |
| |
| SkRect devRect; |
| SkMatrix devToScaledShape; |
| if (localToDevice.rectStaysRect()) { |
| // We can do everything in device space when the src rect projects to a rect in device |
| // space. |
| SkAssertResult(localToDevice.mapRect(&devRect, srcRect)); |
| |
| } else { |
| // The view matrix may scale, perhaps anisotropically. But we want to apply our device space |
| // sigma to the delta of frag coord from the rect edges. Factor out the scaling to define a |
| // space that is purely rotation / translation from device space (and scale from src space). |
| // We'll meet in the middle: pre-scale the src rect to be in this space and then apply the |
| // inverse of the rotation / translation portion to the frag coord. |
| SkMatrix m; |
| SkSize scale; |
| if (!localToDevice.decomposeScale(&scale, &m)) { |
| return std::nullopt; |
| } |
| if (!m.invert(&devToScaledShape)) { |
| return std::nullopt; |
| } |
| devRect = {srcRect.left() * scale.width(), |
| srcRect.top() * scale.height(), |
| srcRect.right() * scale.width(), |
| srcRect.bottom() * scale.height()}; |
| } |
| |
| if (!recorder->priv().caps()->shaderCaps()->fFloatIs32Bits) { |
| // We promote the math that gets us into the Gaussian space to full float when the rect |
| // coords are large. If we don't have full float then fail. We could probably clip the rect |
| // to an outset device bounds instead. |
| if (std::fabs(devRect.left()) > 16000.0f || std::fabs(devRect.top()) > 16000.0f || |
| std::fabs(devRect.right()) > 16000.0f || std::fabs(devRect.bottom()) > 16000.0f) { |
| return std::nullopt; |
| } |
| } |
| |
| const float sixSigma = 6.0f * devSigma; |
| const int tableWidth = ComputeIntegralTableWidth(sixSigma); |
| UniqueKey key; |
| { |
| static const UniqueKey::Domain kRectBlurDomain = UniqueKey::GenerateDomain(); |
| UniqueKey::Builder builder(&key, kRectBlurDomain, 1, "BlurredRectIntegralTable"); |
| builder[0] = tableWidth; |
| } |
| sk_sp<TextureProxy> integral = recorder->priv().proxyCache()->findOrCreateCachedProxy( |
| recorder, key, &tableWidth, |
| [](const void* context) { |
| int tableWidth = *static_cast<const int*>(context); |
| return CreateIntegralTable(tableWidth); |
| }); |
| |
| if (!integral) { |
| return std::nullopt; |
| } |
| |
| // In the fast variant we think of the midpoint of the integral texture as aligning with the |
| // closest rect edge both in x and y. To simplify texture coord calculation we inset the rect so |
| // that the edge of the inset rect corresponds to t = 0 in the texture. It actually simplifies |
| // things a bit in the !isFast case, too. |
| const float threeSigma = 3.0f * devSigma; |
| const Rect shapeData = Rect(devRect.left() + threeSigma, |
| devRect.top() + threeSigma, |
| devRect.right() - threeSigma, |
| devRect.bottom() - threeSigma); |
| |
| // In our fast variant we find the nearest horizontal and vertical edges and for each do a |
| // lookup in the integral texture for each and multiply them. When the rect is less than 6*sigma |
| // wide then things aren't so simple and we have to consider both the left and right edge of the |
| // rectangle (and similar in y). |
| const bool isFast = shapeData.left() <= shapeData.right() && shapeData.top() <= shapeData.bot(); |
| |
| const float invSixSigma = 1.0f / sixSigma; |
| |
| // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
| std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRect); |
| if (!drawBounds) { |
| return std::nullopt; |
| } |
| |
| return AnalyticBlurMask(*drawBounds, |
| SkM44(devToScaledShape), |
| ShapeType::kRect, |
| shapeData, |
| {static_cast<float>(isFast), invSixSigma}, |
| integral); |
| } |
| |
| static float quantize(float deviceSpaceFloat) { |
| // Snap the device-space value to the nearest 1/32 to increase cache hits w/o impacting the |
| // visible output since it should be hard to see a change limited to 1/32 of a pixel. |
| // Clamp the value to 1/32 as identity blurs and points should be caught earlier. |
| return std::max(SkScalarRoundToInt(deviceSpaceFloat * 32.f) / 32.f, 1.f / 32.f); |
| } |
| |
| std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeCircle(Recorder* recorder, |
| const SkMatrix& localToDevice, |
| float devSigma, |
| const SkRect& srcRect, |
| const SkRect& devRect) { |
| const float radius = devRect.width() / 2.0f; |
| if (!SkIsFinite(radius) || radius < SK_ScalarNearlyZero) { |
| return std::nullopt; |
| } |
| |
| // Pack profile-dependent properties and derived values into a struct that can be passed into |
| // findOrCreateCachedProxy to lazily invoke the profile creation bitmap factories. |
| struct DerivedParams { |
| float fQuantizedRadius; |
| float fQuantizedDevSigma; |
| |
| float fSolidRadius; |
| float fTextureRadius; |
| |
| bool fUseHalfPlaneApprox; |
| |
| DerivedParams(float devSigma, float radius) |
| : fQuantizedRadius(quantize(radius)) |
| , fQuantizedDevSigma(quantize(devSigma)) { |
| SkASSERT(fQuantizedRadius > 0.f); // quantization shouldn't have rounded to 0 |
| |
| // When sigma is really small this becomes a equivalent to convolving a Gaussian with a |
| // half-plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to |
| // the Guassian and the profile texture is a just a Gaussian evaluation. However, we |
| // haven't yet implemented this latter optimization. |
| constexpr float kHalfPlaneThreshold = 0.1f; |
| const float sigmaToRadiusRatio = std::min(fQuantizedDevSigma / fQuantizedRadius, 8.0f); |
| if (sigmaToRadiusRatio <= kHalfPlaneThreshold) { |
| fUseHalfPlaneApprox = true; |
| fSolidRadius = fQuantizedRadius - 3.0f * fQuantizedDevSigma; |
| fTextureRadius = 6.0f * fQuantizedDevSigma; |
| } else { |
| fUseHalfPlaneApprox = false; |
| fQuantizedDevSigma = fQuantizedRadius * sigmaToRadiusRatio; |
| fSolidRadius = 0.0f; |
| fTextureRadius = fQuantizedRadius + 3.0f * fQuantizedDevSigma; |
| } |
| } |
| } params{devSigma, radius}; |
| |
| UniqueKey key; |
| { |
| static const UniqueKey::Domain kCircleBlurDomain = UniqueKey::GenerateDomain(); |
| UniqueKey::Builder builder(&key, kCircleBlurDomain, 2, "BlurredCircleIntegralTable"); |
| if (params.fUseHalfPlaneApprox) { |
| // There only ever needs to be one half plane approximation table, so store {0,0} into |
| // the key, which never arises under normal use because we reject radius = 0 above. |
| builder[0] = SkFloat2Bits(0.f); |
| builder[1] = SkFloat2Bits(0.f); |
| } else { |
| builder[0] = SkFloat2Bits(params.fQuantizedDevSigma); |
| builder[1] = SkFloat2Bits(params.fQuantizedRadius); |
| } |
| } |
| sk_sp<TextureProxy> profile = recorder->priv().proxyCache()->findOrCreateCachedProxy( |
| recorder, key, ¶ms, |
| [](const void* context) { |
| constexpr int kProfileTextureWidth = 512; |
| const DerivedParams* params = static_cast<const DerivedParams*>(context); |
| if (params->fUseHalfPlaneApprox) { |
| return CreateHalfPlaneProfile(kProfileTextureWidth); |
| } else { |
| // Rescale params to the size of the texture we're creating. |
| const float scale = kProfileTextureWidth / params->fTextureRadius; |
| return CreateCircleProfile(params->fQuantizedDevSigma * scale, |
| params->fQuantizedRadius * scale, |
| kProfileTextureWidth); |
| } |
| }); |
| |
| if (!profile) { |
| return std::nullopt; |
| } |
| |
| // In the shader we calculate an index into the blur profile |
| // "i = (length(fragCoords - circleCenter) - solidRadius + 0.5) / textureRadius" as |
| // "i = length((fragCoords - circleCenter) / textureRadius) - |
| // (solidRadius - 0.5) / textureRadius" |
| // to avoid passing large values to length() that would overflow. We precalculate |
| // "1 / textureRadius" and "(solidRadius - 0.5) / textureRadius" here. |
| const Rect shapeData = Rect(devRect.centerX(), |
| devRect.centerY(), |
| 1.0f / params.fTextureRadius, |
| (params.fSolidRadius - 0.5f) / params.fTextureRadius); |
| |
| // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
| std::optional<Rect> drawBounds = outset_bounds(localToDevice, |
| params.fQuantizedDevSigma, |
| srcRect); |
| if (!drawBounds) { |
| return std::nullopt; |
| } |
| |
| constexpr float kUnusedBlurData = 0.0f; |
| return AnalyticBlurMask(*drawBounds, |
| SkM44(), |
| ShapeType::kCircle, |
| shapeData, |
| {kUnusedBlurData, kUnusedBlurData}, |
| profile); |
| } |
| |
| std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder, |
| const SkMatrix& localToDevice, |
| float devSigma, |
| const SkRRect& srcRRect, |
| const SkRRect& devRRect) { |
| const int devBlurRadius = 3 * SkScalarCeilToInt(devSigma - 1.0f / 6.0f); |
| |
| const SkVector& devRadiiUL = devRRect.radii(SkRRect::kUpperLeft_Corner); |
| const SkVector& devRadiiUR = devRRect.radii(SkRRect::kUpperRight_Corner); |
| const SkVector& devRadiiLR = devRRect.radii(SkRRect::kLowerRight_Corner); |
| const SkVector& devRadiiLL = devRRect.radii(SkRRect::kLowerLeft_Corner); |
| |
| const int devLeft = SkScalarCeilToInt(std::max<float>(devRadiiUL.fX, devRadiiLL.fX)); |
| const int devTop = SkScalarCeilToInt(std::max<float>(devRadiiUL.fY, devRadiiUR.fY)); |
| const int devRight = SkScalarCeilToInt(std::max<float>(devRadiiUR.fX, devRadiiLR.fX)); |
| const int devBot = SkScalarCeilToInt(std::max<float>(devRadiiLL.fY, devRadiiLR.fY)); |
| |
| // This is a conservative check for nine-patchability. |
| const SkRect& devOrig = devRRect.getBounds(); |
| if (devOrig.fLeft + devLeft + devBlurRadius >= devOrig.fRight - devRight - devBlurRadius || |
| devOrig.fTop + devTop + devBlurRadius >= devOrig.fBottom - devBot - devBlurRadius) { |
| return std::nullopt; |
| } |
| |
| const int newRRWidth = 2 * devBlurRadius + devLeft + devRight + 1; |
| const int newRRHeight = 2 * devBlurRadius + devTop + devBot + 1; |
| |
| const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(devBlurRadius), |
| SkIntToScalar(devBlurRadius), |
| SkIntToScalar(newRRWidth), |
| SkIntToScalar(newRRHeight)); |
| SkVector newRadii[4]; |
| newRadii[0] = {SkScalarCeilToScalar(devRadiiUL.fX), SkScalarCeilToScalar(devRadiiUL.fY)}; |
| newRadii[1] = {SkScalarCeilToScalar(devRadiiUR.fX), SkScalarCeilToScalar(devRadiiUR.fY)}; |
| newRadii[2] = {SkScalarCeilToScalar(devRadiiLR.fX), SkScalarCeilToScalar(devRadiiLR.fY)}; |
| newRadii[3] = {SkScalarCeilToScalar(devRadiiLL.fX), SkScalarCeilToScalar(devRadiiLL.fY)}; |
| |
| // NOTE: SkRRect does not satisfy std::has_unique_object_representation because NaN's in float |
| // values violate that, but all SkRRects that get here will be finite so it's not really a |
| // an issue for hashing the data directly. |
| SK_BEGIN_REQUIRE_DENSE |
| struct DerivedParams { |
| SkRRect fRRectToDraw; |
| SkISize fDimensions; |
| float fDevSigma; |
| } params; |
| SK_END_REQUIRE_DENSE |
| |
| params.fRRectToDraw.setRectRadii(newRect, newRadii); |
| params.fDimensions = |
| SkISize::Make(newRRWidth + 2 * devBlurRadius, newRRHeight + 2 * devBlurRadius); |
| params.fDevSigma = devSigma; |
| |
| // TODO(b/343684954, b/338032240): This is just generating a blurred rrect mask image on the CPU |
| // and uploading it. We should either generate them on the GPU and cache them here, or if we |
| // have a general-purpose blur mask cache, then there's no reason rrects couldn't just use that |
| // since this "analytic" blur isn't actually simplifying work like the circle and rect case. |
| // That would also allow us to support arbitrary blurred rrects and not just ninepatch rrects. |
| static const UniqueKey::Domain kRRectBlurDomain = UniqueKey::GenerateDomain(); |
| UniqueKey key; |
| { |
| static constexpr int kKeySize = sizeof(DerivedParams) / sizeof(uint32_t); |
| static_assert(SkIsAlign4(sizeof(DerivedParams))); |
| // TODO: We should discretize the sigma to perceptibly meaningful changes to the table, |
| // as well as the underlying the round rect geometry. |
| UniqueKey::Builder builder(&key, kRRectBlurDomain, kKeySize, "BlurredRRectNinePatch"); |
| memcpy(&builder[0], ¶ms, sizeof(DerivedParams)); |
| } |
| |
| sk_sp<TextureProxy> ninePatch = recorder->priv().proxyCache()->findOrCreateCachedProxy( |
| recorder, key, ¶ms, |
| [](Recorder* r, const void* context) -> sk_sp<Image> { |
| const DerivedParams* params = static_cast<const DerivedParams*>(context); |
| |
| const SkImageInfo rrectII = SkImageInfo::MakeA8(params->fDimensions.width(), |
| params->fDimensions.height()); |
| sk_sp<Surface> surface = Surface::MakeScratch(r, rrectII, "BlurredRRectNinePatch", |
| Budgeted::kYes, Mipmapped::kNo, |
| SkBackingFit::kExact); // for now... |
| if (!surface) { |
| return nullptr; |
| } |
| // Use an image filter directly, not a mask filter, so we don't get stuck in a loop |
| SkPaint blurRRectPaint; |
| blurRRectPaint.setImageFilter(SkImageFilters::Blur(params->fDevSigma, |
| params->fDevSigma, |
| nullptr)); |
| blurRRectPaint.setBlendMode(SkBlendMode::kSrc); |
| surface->getCanvas()->drawRRect(params->fRRectToDraw, blurRRectPaint); |
| return surface->asImage(); |
| }); |
| |
| if (!ninePatch) { |
| return std::nullopt; |
| } |
| |
| const float blurRadius = 3.0f * SkScalarCeilToScalar(devSigma - 1.0f / 6.0f); |
| const float edgeSize = 2.0f * blurRadius + SkRRectPriv::GetSimpleRadii(devRRect).fX + 0.5f; |
| const Rect shapeData = devRRect.rect().makeOutset(blurRadius, blurRadius); |
| |
| // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
| std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRRect.rect()); |
| if (!drawBounds) { |
| return std::nullopt; |
| } |
| |
| constexpr float kUnusedBlurData = 0.0f; |
| return AnalyticBlurMask(*drawBounds, |
| SkM44(), |
| ShapeType::kRRect, |
| shapeData, |
| {edgeSize, kUnusedBlurData}, |
| ninePatch); |
| } |
| |
| } // namespace skgpu::graphite |