diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc index f181ea9f0c..84c9dc2e09 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc @@ -34,6 +34,56 @@ namespace testing { using namespace flutter; +// The shapes of these ovals should appear equal. They are demonstrating the +// difference between the fast pass and not. +TEST_P(AiksTest, SolidColorOvalsMaskBlurTinySigma) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + std::vector sigmas = {0.0, 0.01, 1.0}; + std::vector colors = {DlColor::kGreen(), DlColor::kYellow(), + DlColor::kRed()}; + for (uint32_t i = 0; i < sigmas.size(); ++i) { + DlPaint paint; + paint.setColor(colors[i]); + paint.setMaskFilter( + DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i])); + + builder.Save(); + builder.Translate(100 + (i * 100), 100); + SkRRect rrect = + SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 60.0f, 160.0f), 50, 100); + builder.DrawRRect(rrect, paint); + builder.Restore(); + } + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, SolidColorCircleMaskBlurTinySigma) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + std::vector sigmas = {0.0, 0.01, 1.0}; + std::vector colors = {DlColor::kGreen(), DlColor::kYellow(), + DlColor::kRed()}; + for (uint32_t i = 0; i < sigmas.size(); ++i) { + DlPaint paint; + paint.setColor(colors[i]); + paint.setMaskFilter( + DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i])); + + builder.Save(); + builder.Translate(100 + (i * 100), 100); + SkRRect rrect = + SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 100.0f, 100.0f), 100, 100); + builder.DrawRRect(rrect, paint); + builder.Restore(); + } + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + TEST_P(AiksTest, CanRenderMaskBlurHugeSigma) { DisplayListBuilder builder; diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc index 626ff8114c..1fe68bb5b8 100644 --- a/engine/src/flutter/impeller/display_list/canvas.cc +++ b/engine/src/flutter/impeller/display_list/canvas.cc @@ -464,6 +464,11 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect, return false; } + // The current rrect blur math doesn't work on ovals. + if (fabsf(corner_radii.width - corner_radii.height) > kEhCloseEnough) { + return false; + } + // For symmetrically mask blurred solid RRects, absorb the mask blur and use // a faster SDF approximation. diff --git a/engine/src/flutter/impeller/entity/contents/solid_rrect_blur_contents.cc b/engine/src/flutter/impeller/entity/contents/solid_rrect_blur_contents.cc index 43144a5f29..20f0172358 100644 --- a/engine/src/flutter/impeller/entity/contents/solid_rrect_blur_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/solid_rrect_blur_contents.cc @@ -48,6 +48,56 @@ Color SolidRRectBlurContents::GetColor() const { return color_; } +static Point eccentricity(Point v, double sInverse) { + Point vOverS = v * sInverse * 0.5; + Point vOverS_squared = -(vOverS * vOverS); + return {std::exp(vOverS_squared.x), std::exp(vOverS_squared.y)}; +} + +static Scalar kTwoOverSqrtPi = 2.0 / std::sqrt(kPi); + +// use crate::math::compute_erf7; +static Scalar computeErf7(Scalar x) { + x *= kTwoOverSqrtPi; + float xx = x * x; + x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx); + return x / sqrt(1.0 + x * x); +} + +static Point NegPos(Scalar v) { + return {std::min(v, 0.0f), std::max(v, 0.0f)}; +} + +static void SetupFragInfo( + RRectBlurPipeline::FragmentShader::FragInfo& frag_info, + Scalar blurSigma, + Point center, + Point rSize, + Scalar radius) { + Scalar sigma = std::max(blurSigma * kSqrt2, 1.f); + + frag_info.center = rSize * 0.5f; + frag_info.minEdge = std::min(rSize.x, rSize.y); + double rMax = 0.5 * frag_info.minEdge; + double r0 = std::min(std::hypot(radius, sigma * 1.15), rMax); + frag_info.r1 = std::min(std::hypot(radius, sigma * 2.0), rMax); + + frag_info.exponent = 2.0 * frag_info.r1 / r0; + + frag_info.sInv = 1.0 / sigma; + + // Pull in long end (make less eccentric). + Point eccentricV = eccentricity(rSize, frag_info.sInv); + double delta = 1.25 * sigma * (eccentricV.x - eccentricV.y); + rSize += NegPos(delta); + + frag_info.adjust = rSize * 0.5 - frag_info.r1; + frag_info.exponentInv = 1.0 / frag_info.exponent; + frag_info.scale = + 0.5 * computeErf7(frag_info.sInv * 0.5 * + (std::max(rSize.x, rSize.y) - 0.5 * radius)); +} + std::optional SolidRRectBlurContents::GetCoverage( const Entity& entity) const { if (!rect_.has_value()) { @@ -72,15 +122,15 @@ bool SolidRRectBlurContents::Render(const ContentContext& renderer, // Clamp the max kernel width/height to 1000 to limit the extent // of the blur and to kEhCloseEnough to prevent NaN calculations // trying to evaluate a Guassian distribution with a sigma of 0. - auto blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f); + Scalar blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f); // Increase quality by making the radius a bit bigger than the typical // sigma->radius conversion we use for slower blurs. - auto blur_radius = PadForSigma(blur_sigma); - auto positive_rect = rect_->GetPositive(); - auto left = -blur_radius; - auto top = -blur_radius; - auto right = positive_rect.GetWidth() + blur_radius; - auto bottom = positive_rect.GetHeight() + blur_radius; + Scalar blur_radius = PadForSigma(blur_sigma); + Rect positive_rect = rect_->GetPositive(); + Scalar left = -blur_radius; + Scalar top = -blur_radius; + Scalar right = positive_rect.GetWidth() + blur_radius; + Scalar bottom = positive_rect.GetHeight() + blur_radius; std::array vertices = { VS::PerVertexData{Point(left, top)}, @@ -105,12 +155,12 @@ bool SolidRRectBlurContents::Render(const ContentContext& renderer, FS::FragInfo frag_info; frag_info.color = color; - frag_info.blur_sigma = blur_sigma; - frag_info.rect_size = Point(positive_rect.GetSize()); - frag_info.corner_radii = {std::clamp(corner_radii_.width, kEhCloseEnough, - positive_rect.GetWidth() * 0.5f), - std::clamp(corner_radii_.height, kEhCloseEnough, - positive_rect.GetHeight() * 0.5f)}; + Scalar radius = std::min(std::clamp(corner_radii_.width, kEhCloseEnough, + positive_rect.GetWidth() * 0.5f), + std::clamp(corner_radii_.height, kEhCloseEnough, + positive_rect.GetHeight() * 0.5f)); + SetupFragInfo(frag_info, blur_sigma, positive_rect.GetCenter(), + Point(positive_rect.GetSize()), radius); auto& host_buffer = renderer.GetTransientsBuffer(); pass.SetCommandLabel("RRect Shadow"); pass.SetPipeline(renderer.GetRRectBlurPipeline(opts)); diff --git a/engine/src/flutter/impeller/entity/shaders/rrect_blur.frag b/engine/src/flutter/impeller/entity/shaders/rrect_blur.frag index a4574ad7b5..d0bba43561 100644 --- a/engine/src/flutter/impeller/entity/shaders/rrect_blur.frag +++ b/engine/src/flutter/impeller/entity/shaders/rrect_blur.frag @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// The math for this shader was based on the work done in Raph Levien's blog +// post "Blurred rounded rectangles": +// https://web.archive.org/web/20231103044404/https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html + precision highp float; #include @@ -9,9 +13,14 @@ precision highp float; uniform FragInfo { f16vec4 color; - vec2 rect_size; - float blur_sigma; - vec2 corner_radii; + vec2 center; + vec2 adjust; + float minEdge; + float r1; + float exponent; + float sInv; + float exponentInv; + float scale; } frag_info; @@ -19,94 +28,36 @@ in vec2 v_position; out f16vec4 frag_color; -const int kSampleCount = 4; +const float kTwoOverSqrtPi = 2.0 / sqrt(3.1415926); -/// Closed form unidirectional rounded rect blur mask solution using the -/// analytical Gaussian integral (with approximated erf). -vec4 RRectBlurX(float sample_position_x, - vec4 sample_position_y, - vec2 half_size) { - // The vertical edge of the rrect consists of a flat portion and a curved - // portion, the two of which vary in size depending on the size of the - // corner radii, both adding up to half_size.y. - // half_size.y - corner_radii.y is the size of the vertical flat - // portion of the rrect. - // subtracting the absolute value of the Y sample_position will be - // negative (and then clamped to 0) for positions that are located - // vertically in the flat part of the rrect, and will be the relative - // distance from the center of curvature otherwise. - vec4 space_y = min(vec4(0.0), half_size.y - frag_info.corner_radii.y - - abs(sample_position_y)); - // space is now in the range [0.0, corner_radii.y]. If the y sample was - // in the flat portion of the rrect, it will be 0.0 - - // We will now calculate rrect_distance as the distance from the centerline - // of the rrect towards the near side of the rrect. - // half_size.x - frag_info.corner_radii.x is the size of the horizontal - // flat portion of the rrect. - // We add to that the X size (space_x) of the curved corner measured at - // the indicated Y coordinate we calculated as space_y, such that: - // (space_y / corner_radii.y)^2 + (space_x / corner_radii.x)^2 == 1.0 - // Since we want the space_x, we rearrange the equation as: - // space_x = corner_radii.x * sqrt(1.0 - (space_y / corner_radii.y)^2) - // We need to prevent negative values inside the sqrt which can occur - // when the Y sample was beyond the vertical size of the rrect and thus - // space_y was larger than corner_radii.y. - // The calling function RRectBlur will never provide a Y sample outside - // of that range, though, so the max(0.0) is mostly a precaution. - vec4 unit_space_y = space_y / frag_info.corner_radii.y; - vec4 unit_space_x = sqrt(max(vec4(0.0), 1.0 - unit_space_y * unit_space_y)); - vec4 rrect_distance = - half_size.x - frag_info.corner_radii.x * (1.0 - unit_space_x); - - vec4 result; - // Now we integrate the Gaussian over the range of the relative positions - // of the left and right sides of the rrect relative to the sampling - // X coordinate. - vec4 integral = IPVec4FastGaussianIntegral( - float(sample_position_x) + vec4(-rrect_distance[0], rrect_distance[0], - -rrect_distance[1], rrect_distance[1]), - float(frag_info.blur_sigma)); - // integral.y contains the evaluation of the indefinite gaussian integral - // function at (X + rrect_distance) and integral.x contains the evaluation - // of it at (X - rrect_distance). Subtracting the two produces the - // integral result over the range from one to the other. - result.xy = integral.yw - integral.xz; - integral = IPVec4FastGaussianIntegral( - float(sample_position_x) + vec4(-rrect_distance[2], rrect_distance[2], - -rrect_distance[3], rrect_distance[3]), - float(frag_info.blur_sigma)); - result.zw = integral.yw - integral.xz; - - return result; +float maxXY(vec2 v) { + return max(v.x, v.y); } -float RRectBlur(vec2 sample_position, vec2 half_size) { - // Limit the sampling range to 3 standard deviations in the Y direction from - // the kernel center to incorporate 99.7% of the color contribution. - float half_sampling_range = frag_info.blur_sigma * 3.0; +// use crate::math::compute_erf7; +float computeErf7(float x) { + x *= kTwoOverSqrtPi; + float xx = x * x; + x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx); + return x / sqrt(1.0 + x * x); +} - // We want to cover the range [Y - half_range, Y + half_range], but we - // don't want to sample beyond the edge of the rrect (where the RRectBlurX - // function produces bad information and where the real answer at those - // locations will be 0.0 anyway). - float begin_y = max(-half_sampling_range, sample_position.y - half_size.y); - float end_y = min(half_sampling_range, sample_position.y + half_size.y); - float interval = (end_y - begin_y) / kSampleCount; - - // Sample the X blur kSampleCount times, weighted by the Gaussian function. - vec4 ys = vec4(0.5, 1.5, 2.5, 3.5) * interval + begin_y; - vec4 sample_ys = sample_position.y - ys; - vec4 blurx = RRectBlurX(sample_position.x, sample_ys, half_size); - vec4 gaussian_y = IPGaussian(ys, float(frag_info.blur_sigma)); - return dot(blurx, gaussian_y * interval); +// The length formula, but with an exponent other than 2 +float powerDistance(vec2 p) { + float xp = pow(p.x, frag_info.exponent); + float yp = pow(p.y, frag_info.exponent); + return pow(xp + yp, frag_info.exponentInv); } void main() { - frag_color = frag_info.color; + vec2 adjusted = abs(v_position - frag_info.center) - frag_info.adjust; - vec2 half_size = frag_info.rect_size * 0.5; - vec2 sample_position = v_position - half_size; + float dPos = powerDistance(max(adjusted, 0.0)); + float dNeg = min(maxXY(adjusted), 0.0); + float d = dPos + dNeg - frag_info.r1; + float z = + frag_info.scale * (computeErf7(frag_info.sInv * (frag_info.minEdge + d)) - + computeErf7(frag_info.sInv * d)); - frag_color *= float16_t(RRectBlur(sample_position, half_size)); + frag_color = frag_info.color * float16_t(z); } diff --git a/engine/src/flutter/impeller/geometry/point.h b/engine/src/flutter/impeller/geometry/point.h index 02df4dce11..2c19467c64 100644 --- a/engine/src/flutter/impeller/geometry/point.h +++ b/engine/src/flutter/impeller/geometry/point.h @@ -319,6 +319,11 @@ constexpr TPoint operator/(const TSize& s, const TPoint& p) { return {static_cast(s.width) / p.x, static_cast(s.height) / p.y}; } +template +constexpr TPoint operator-(const TPoint& p, T v) { + return {p.x - v, p.y - v}; +} + using Point = TPoint; using IPoint = TPoint; using IPoint32 = TPoint; diff --git a/engine/src/flutter/impeller/tools/malioc.json b/engine/src/flutter/impeller/tools/malioc.json index 85c0c6effd..aafabbc2af 100644 --- a/engine/src/flutter/impeller/tools/malioc.json +++ b/engine/src/flutter/impeller/tools/malioc.json @@ -3858,10 +3858,10 @@ "arith_fma" ], "longest_path_cycles": [ - 1.59375, - 1.59375, - 0.453125, - 1.5, + 0.625, + 0.625, + 0.21875, + 0.5, 0.0, 0.25, 0.0 @@ -3880,10 +3880,10 @@ "arith_fma" ], "shortest_path_cycles": [ - 1.59375, - 1.59375, - 0.421875, - 1.5, + 0.625, + 0.625, + 0.1875, + 0.5, 0.0, 0.25, 0.0 @@ -3893,10 +3893,10 @@ "arith_fma" ], "total_cycles": [ - 1.59375, - 1.59375, - 0.453125, - 1.5, + 0.625, + 0.625, + 0.21875, + 0.5, 0.0, 0.25, 0.0 @@ -3904,8 +3904,8 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 22, - "work_registers_used": 32 + "uniform_registers_used": 18, + "work_registers_used": 23 } } }, @@ -3922,7 +3922,7 @@ "arithmetic" ], "longest_path_cycles": [ - 12.869999885559082, + 6.599999904632568, 1.0, 0.0 ], @@ -3935,7 +3935,7 @@ "arithmetic" ], "shortest_path_cycles": [ - 12.869999885559082, + 6.599999904632568, 1.0, 0.0 ], @@ -3943,14 +3943,14 @@ "arithmetic" ], "total_cycles": [ - 13.333333015441895, + 7.0, 1.0, 0.0 ] }, - "thread_occupancy": 50, - "uniform_registers_used": 3, - "work_registers_used": 7 + "thread_occupancy": 100, + "uniform_registers_used": 4, + "work_registers_used": 3 } } } @@ -6816,10 +6816,10 @@ "arith_fma" ], "longest_path_cycles": [ - 1.59375, - 1.59375, - 0.421875, - 1.5, + 0.6875, + 0.6875, + 0.1875, + 0.5, 0.0, 0.25, 0.0 @@ -6838,10 +6838,10 @@ "arith_fma" ], "shortest_path_cycles": [ - 1.59375, - 1.59375, - 0.421875, - 1.5, + 0.6875, + 0.6875, + 0.1875, + 0.5, 0.0, 0.25, 0.0 @@ -6851,10 +6851,10 @@ "arith_fma" ], "total_cycles": [ - 1.59375, - 1.59375, - 0.421875, - 1.5, + 0.6875, + 0.6875, + 0.1875, + 0.5, 0.0, 0.25, 0.0 @@ -6862,8 +6862,8 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 22, - "work_registers_used": 32 + "uniform_registers_used": 18, + "work_registers_used": 13 } } } diff --git a/engine/src/flutter/testing/impeller_golden_tests_output.txt b/engine/src/flutter/testing/impeller_golden_tests_output.txt index 860243710c..7d56e0f10a 100644 --- a/engine/src/flutter/testing/impeller_golden_tests_output.txt +++ b/engine/src/flutter/testing/impeller_golden_tests_output.txt @@ -813,9 +813,15 @@ impeller_Play_AiksTest_SetContentsWithRegion_Vulkan.png impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_Metal.png impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_OpenGLES.png impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_Vulkan.png +impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_Metal.png +impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_OpenGLES.png +impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_Vulkan.png impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_Metal.png impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_OpenGLES.png impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_Vulkan.png +impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_Metal.png +impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_OpenGLES.png +impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_Vulkan.png impeller_Play_AiksTest_SolidStrokesRenderCorrectly_Metal.png impeller_Play_AiksTest_SolidStrokesRenderCorrectly_OpenGLES.png impeller_Play_AiksTest_SolidStrokesRenderCorrectly_Vulkan.png