[Impeller] Clamp new blur sigma (flutter/engine#48813)

fixes `AiksTests.CanRenderBackdropBlurHugeSigma/Metal`

I also tweaked the existing quadratic equation to make sure the minima
is at 500 and `f(0) = 1`.

## before

<img width="1024" alt="Screenshot 2023-12-07 at 4 36 40 PM"
src="https://github.com/flutter/engine/assets/30870216/2f32388d-6960-47b2-a690-5e5be2cd4a9a">


## after
<img width="1022" alt="Screenshot 2023-12-07 at 4 32 11 PM"
src="https://github.com/flutter/engine/assets/30870216/b1855400-656d-41eb-858c-56fb7a1ab4cf">

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat

---------

Co-authored-by: Jonah Williams <jonahwilliams@google.com>
This commit is contained in:
gaaclarke 2023-12-08 16:57:40 -08:00 committed by GitHub
parent f4f06d00fb
commit e315f81ba0
4 changed files with 88 additions and 24 deletions

View File

@ -24,20 +24,13 @@
namespace impeller {
// This function was derived with polynomial regression when comparing the
// results with Skia. Changing the curve below should invalidate this.
//
// The following data points were used:
// 0 | 1
// 75 | 0.8
// 150 | 0.5
// 300 | 0.22
// 400 | 0.2
// 500 | 0.15
// This function was calculated by observing Skia's behavior. Its blur at 500
// seemed to be 0.15. Since we clamp at 500 I solved the quadratic equation
// that puts the minima there and a f(0)=1.
Sigma ScaleSigma(Sigma sigma) {
// Limit the kernel size to 1000x1000 pixels, like Skia does.
Scalar clamped = std::min(sigma.sigma, 500.0f);
Scalar scalar = 1.02 - 3.89e-3 * clamped + 4.36e-06 * clamped * clamped;
Scalar scalar = 1.0 - 3.4e-3 * clamped + 3.4e-06 * clamped * clamped;
return Sigma(clamped * scalar);
}

View File

@ -210,7 +210,8 @@ Scalar GaussianBlurFilterContents::CalculateScale(Scalar sigma) {
std::optional<Rect> GaussianBlurFilterContents::GetFilterSourceCoverage(
const Matrix& effect_transform,
const Rect& output_limit) const {
Scalar blur_radius = CalculateBlurRadius(sigma_);
Scalar scaled_sigma = ScaleSigma(sigma_);
Scalar blur_radius = CalculateBlurRadius(scaled_sigma);
Vector3 blur_radii =
effect_transform.Basis() * Vector3{blur_radius, blur_radius, 0.0};
return output_limit.Expand(Point(blur_radii.x, blur_radii.y));
@ -229,7 +230,8 @@ std::optional<Rect> GaussianBlurFilterContents::GetFilterCoverage(
return {};
}
Scalar blur_radius = CalculateBlurRadius(sigma_);
Scalar scaled_sigma = ScaleSigma(sigma_);
Scalar blur_radius = CalculateBlurRadius(scaled_sigma);
Vector3 blur_radii =
(inputs[0]->GetTransform(entity).Basis() * effect_transform.Basis() *
Vector3{blur_radius, blur_radius, 0.0})
@ -248,7 +250,8 @@ std::optional<Entity> GaussianBlurFilterContents::RenderFilter(
return std::nullopt;
}
Scalar blur_radius = CalculateBlurRadius(sigma_);
Scalar scaled_sigma = ScaleSigma(sigma_);
Scalar blur_radius = CalculateBlurRadius(scaled_sigma);
Vector2 padding(ceil(blur_radius), ceil(blur_radius));
// Apply as much of the desired padding as possible from the source. This may
@ -269,12 +272,12 @@ std::optional<Entity> GaussianBlurFilterContents::RenderFilter(
return std::nullopt;
}
if (sigma_ < kEhCloseEnough) {
if (scaled_sigma < kEhCloseEnough) {
return Entity::FromSnapshot(input_snapshot.value(), entity.GetBlendMode(),
entity.GetClipDepth()); // No blur to render.
}
Scalar desired_scalar = CalculateScale(sigma_);
Scalar desired_scalar = CalculateScale(scaled_sigma);
// TODO(jonahwilliams): If desired_scalar is 1.0 and we fully acquired the
// gutter from the expanded_coverage_hint, we can skip the downsample pass.
// pass.
@ -301,7 +304,7 @@ std::optional<Entity> GaussianBlurFilterContents::RenderFilter(
renderer, pass1_out_texture, input_snapshot->sampler_descriptor,
GaussianBlurFragmentShader::BlurInfo{
.blur_uv_offset = Point(0.0, pass1_pixel_size.y),
.blur_sigma = sigma_ * effective_scalar.y,
.blur_sigma = scaled_sigma * effective_scalar.y,
.blur_radius = blur_radius * effective_scalar.y,
.step_size = 1.0,
});
@ -311,7 +314,7 @@ std::optional<Entity> GaussianBlurFilterContents::RenderFilter(
renderer, pass2_out_texture, input_snapshot->sampler_descriptor,
GaussianBlurFragmentShader::BlurInfo{
.blur_uv_offset = Point(pass1_pixel_size.x, 0.0),
.blur_sigma = sigma_ * effective_scalar.x,
.blur_sigma = scaled_sigma * effective_scalar.x,
.blur_radius = blur_radius * effective_scalar.x,
.step_size = 1.0,
});
@ -348,4 +351,17 @@ Quad GaussianBlurFilterContents::CalculateUVs(
return uv_transform.Transform(coverage_quad);
}
// This function was calculated by observing Skia's behavior. Its blur at 500
// seemed to be 0.15. Since we clamp at 500 I solved the quadratic equation
// that puts the minima there and a f(0)=1.
Scalar GaussianBlurFilterContents::ScaleSigma(Scalar sigma) {
// Limit the kernel size to 1000x1000 pixels, like Skia does.
Scalar clamped = std::min(sigma, 500.0f);
constexpr Scalar a = 3.4e-06;
constexpr Scalar b = -3.4e-3;
constexpr Scalar c = 1.f;
Scalar scalar = c + b * clamped + a * clamped * clamped;
return clamped * scalar;
}
} // namespace impeller

View File

@ -47,6 +47,15 @@ class GaussianBlurFilterContents final : public FilterContents {
/// Visible for testing.
static Scalar CalculateScale(Scalar sigma);
/// Scales down the sigma value to match Skia's behavior.
///
/// effective_blur_radius = CalculateBlurRadius(ScaleSigma(sigma_));
///
/// This function was calculated by observing Skia's behavior. Its blur at
/// 500 seemed to be 0.15. Since we clamp at 500 I solved the quadratic
/// equation that puts the minima there and a f(0)=1.
static Scalar ScaleSigma(Scalar sigma);
private:
// |FilterContents|
std::optional<Entity> RenderFilter(

View File

@ -16,9 +16,36 @@ namespace testing {
namespace {
Scalar CalculateSigmaForBlurRadius(Scalar blur_radius) {
// See Sigma.h
return (blur_radius / kKernelRadiusPerSigma) + 0.5;
// Use newtonian method to give the closest answer to target where
// f(x) is less than the target. We do this because the value is `ceil`'d to
// grab fractional pixels.
float LowerBoundNewtonianMethod(const std::function<float(float)>& func,
float target,
float guess,
float tolerance) {
const float delta = 1e-6;
float x = guess;
float fx;
do {
fx = func(x) - target;
float derivative = (func(x + delta) - func(x)) / delta;
x = x - fx / derivative;
} while (std::abs(fx) > tolerance ||
fx < 0.0); // fx < 0.0 makes this lower bound.
return x;
}
Scalar CalculateSigmaForBlurRadius(Scalar radius) {
auto f = [](Scalar x) -> Scalar {
return GaussianBlurFilterContents::CalculateBlurRadius(
GaussianBlurFilterContents::ScaleSigma(x));
};
// The newtonian method is used here since inverting the function is
// non-trivial because of conditional logic and would be fragile to changes.
return LowerBoundNewtonianMethod(f, radius, 2.f, 0.001f);
}
} // namespace
@ -67,7 +94,10 @@ TEST(GaussianBlurFilterContentsTest, CoverageWithSigma) {
Entity entity;
std::optional<Rect> coverage =
contents.GetFilterCoverage(inputs, entity, /*effect_transform=*/Matrix());
ASSERT_EQ(coverage, Rect::MakeLTRB(99, 99, 201, 201));
EXPECT_TRUE(coverage.has_value());
if (coverage.has_value()) {
EXPECT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(99, 99, 201, 201));
}
}
TEST_P(GaussianBlurFilterContentsTest, CoverageWithTexture) {
@ -87,7 +117,10 @@ TEST_P(GaussianBlurFilterContentsTest, CoverageWithTexture) {
entity.SetTransform(Matrix::MakeTranslation({100, 100, 0}));
std::optional<Rect> coverage =
contents.GetFilterCoverage(inputs, entity, /*effect_transform=*/Matrix());
ASSERT_EQ(coverage, Rect::MakeLTRB(99, 99, 201, 201));
EXPECT_TRUE(coverage.has_value());
if (coverage.has_value()) {
EXPECT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(99, 99, 201, 201));
}
}
TEST_P(GaussianBlurFilterContentsTest, CoverageWithEffectTransform) {
@ -107,7 +140,11 @@ TEST_P(GaussianBlurFilterContentsTest, CoverageWithEffectTransform) {
entity.SetTransform(Matrix::MakeTranslation({100, 100, 0}));
std::optional<Rect> coverage = contents.GetFilterCoverage(
inputs, entity, /*effect_transform=*/Matrix::MakeScale({2.0, 2.0, 1.0}));
ASSERT_EQ(coverage, Rect::MakeLTRB(100 - 2, 100 - 2, 200 + 2, 200 + 2));
EXPECT_TRUE(coverage.has_value());
if (coverage.has_value()) {
EXPECT_RECT_NEAR(coverage.value(),
Rect::MakeLTRB(100 - 2, 100 - 2, 200 + 2, 200 + 2));
}
}
TEST(GaussianBlurFilterContentsTest, FilterSourceCoverage) {
@ -328,5 +365,14 @@ TEST_P(GaussianBlurFilterContentsTest,
}
}
TEST(GaussianBlurFilterContentsTest, CalculateSigmaForBlurRadius) {
Scalar sigma = 1.0;
Scalar radius = GaussianBlurFilterContents::CalculateBlurRadius(
GaussianBlurFilterContents::ScaleSigma(sigma));
Scalar derived_sigma = CalculateSigmaForBlurRadius(radius);
EXPECT_NEAR(sigma, derived_sigma, 0.01f);
}
} // namespace testing
} // namespace impeller