From 445db0da1d682609751385551a44927a167e9f80 Mon Sep 17 00:00:00 2001 From: ColdPaleLight Date: Tue, 6 Jun 2023 11:55:39 +0800 Subject: [PATCH] [Impeller] Make conical gradient work as expected (#42567) fix https://github.com/flutter/flutter/issues/128012 In many instances (https://github.com/flutter/flutter/issues/128012#issuecomment-1577029031), the current implementation of conical gradient in impeller produces incorrect results. This pull request proposes to migrate the conical gradient algorithm from skia to impeller. Please see https://github.com/google/skia/blob/ddf987d2ab3314ee0e80ac1ae7dbffb44a87d394/src/sksl/sksl_graphite_frag.sksl#L541-L666 Test result ![FGcZZRCQZJ](https://github.com/flutter/engine/assets/31977171/4127e73d-81a9-4d3e-8430-cf511367362c) --- impeller/aiks/aiks_unittests.cc | 42 ++++++ .../shader_lib/impeller/gradient.glsl | 134 +++++++++++++++--- .../contents/conical_gradient_contents.cc | 12 -- .../contents/conical_gradient_contents.h | 3 - impeller/entity/entity_unittests.cc | 2 +- .../entity/shaders/conical_gradient_fill.frag | 11 +- .../shaders/conical_gradient_ssbo_fill.frag | 10 +- impeller/tools/malioc.json | 111 ++++++++------- 8 files changed, 231 insertions(+), 94 deletions(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index dd4f4f31ec1b6..a45622d58fb53 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -749,6 +749,48 @@ TEST_P(AiksTest, CanRenderSweepGradientManyColorsDecal) { CanRenderSweepGradientManyColors(this, Entity::TileMode::kDecal); } +TEST_P(AiksTest, CanRenderConicalGradient) { + Scalar size = 256; + Canvas canvas; + Paint paint; + paint.color = Color::White(); + canvas.DrawRect({0, 0, size * 3, size * 3}, paint); + std::vector colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF), + Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF), + Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF), + Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)}; + std::vector stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0}; + std::array, 8> array{ + std::make_tuple(Point{size / 2.f, size / 2.f}, 0.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 2.f, size / 2.f}, size / 4.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, 0.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 2.f, + Point{size / 2.f, size / 2.f}, 0), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 4.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 16.f, + Point{size / 2.f, size / 2.f}, size / 8.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 8.f, + Point{size / 2.f, size / 2.f}, size / 16.f), + std::make_tuple(Point{size / 8.f, size / 8.f}, size / 8.f, + Point{size / 2.f, size / 2.f}, size / 8.f), + }; + for (int i = 0; i < 8; i++) { + canvas.Save(); + canvas.Translate({(i % 3) * size, i / 3 * size, 0}); + paint.color_source = ColorSource::MakeConicalGradient( + std::get<0>(array[i]), std::get<1>(array[i]), colors, stops, + std::get<2>(array[i]), std::get<3>(array[i]), Entity::TileMode::kClamp, + {}); + canvas.DrawRect({0, 0, size, size}, paint); + canvas.Restore(); + } + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + TEST_P(AiksTest, CanRenderDifferentShapesWithSameColorSource) { Canvas canvas; Paint paint; diff --git a/impeller/compiler/shader_lib/impeller/gradient.glsl b/impeller/compiler/shader_lib/impeller/gradient.glsl index a82da66ad7490..871b4646ebdeb 100644 --- a/impeller/compiler/shader_lib/impeller/gradient.glsl +++ b/impeller/compiler/shader_lib/impeller/gradient.glsl @@ -7,27 +7,127 @@ #include +mat3 IPMapToUnitX(vec2 p0, vec2 p1) { + // Returns a matrix that maps [p0, p1] to [(0, 0), (1, 0)]. Results are + // undefined if p0 = p1. + return mat3(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0) * + inverse(mat3(p1.y - p0.y, p0.x - p1.x, 0.0, p1.x - p0.x, p1.y - p0.y, + 0.0, p0.x, p0.y, 1.0)); +} + /// Compute the t value for a conical gradient at point `p` between the 2 -/// circles defined by (c0, r0) and (c1, r1). +/// circles defined by (c0, r0) and (c1, r1). The returned vec2 encapsulates 't' +/// as its x component and validity status as its y component, with positive y +/// indicating a valid result. /// -/// This assumes that c0 != c1. -float IPComputeConicalT(vec2 c0, float r0, vec2 c1, float r1, vec2 p) { - float w = 1.0; - float result = 0.0; - vec2 ab = c1 - c0; - float dr = r1 - r0; - // Set sample rate to a minimum for the case where c0 and c1 are close. - float delta = 1.0 / max(length(ab), 100.0); - while (w >= 0.0) { - vec2 cw = w * ab + c0; - float rw = w * dr + r0; - if (length(p - cw) <= rw) { - result = w; - break; +/// The code is migrated from Skia Graphite. See +/// https://github.com/google/skia/blob/ddf987d2ab3314ee0e80ac1ae7dbffb44a87d394/src/sksl/sksl_graphite_frag.sksl#L541-L666. +vec2 IPComputeConicalT(vec2 c0, float r0, vec2 c1, float r1, vec2 pos) { + const float scalar_nearly_zero = 1.0 / float(1 << 12); + float d_center = distance(c0, c1); + float d_radius = r1 - r0; + + // Degenerate case: a radial gradient (p0 = p1). + bool radial = d_center < scalar_nearly_zero; + + // Degenerate case: a strip with bandwidth 2r (r0 = r1). + bool strip = abs(d_radius) < scalar_nearly_zero; + + if (radial) { + if (strip) { + // The start and end inputs are the same in both position and radius. + // We don't expect to see this input, but just in case we avoid dividing + // by zero. + return vec2(0.0, -1.0); + } + + float scale = 1.0 / d_radius; + float scale_sign = sign(d_radius); + float bias = r0 / d_radius; + + vec2 pt = (pos - c0) * scale; + float t = length(pt) * scale_sign - bias; + return vec2(t, 1.0); + + } else if (strip) { + mat3 transform = IPMapToUnitX(c0, c1); + float r = r0 / d_center; + float r_2 = r * r; + + vec2 pt = (transform * vec3(pos.xy, 1.0)).xy; + float t = r_2 - pt.y * pt.y; + if (t < 0.0) { + return vec2(0.0, -1.0); + } + t = pt.x + sqrt(t); + return vec2(t, 1.0); + + } else { + // See https://skia.org/docs/dev/design/conical/ for details on how this + // algorithm works. Calculate f and swap inputs if necessary (steps 1 and + // 2). + float f = r0 / (r0 - r1); + + bool is_swapped = abs(f - 1.0) < scalar_nearly_zero; + if (is_swapped) { + vec2 tmp_pt = c0; + c0 = c1; + c1 = tmp_pt; + f = 0.0; + } + + // Apply mapping from [Cf, C1] to unit x, and apply the precalculations from + // steps 3 and 4, all in the same transformation. + vec2 cf = c0 * (1.0 - f) + c1 * f; + mat3 transform = IPMapToUnitX(cf, c1); + + float scale_x = abs(1.0 - f); + float scale_y = scale_x; + float r1 = abs(r1 - r0) / d_center; + bool is_focal_on_circle = abs(r1 - 1.0) < scalar_nearly_zero; + if (is_focal_on_circle) { + scale_x *= 0.5; + scale_y *= 0.5; + } else { + scale_x *= r1 / (r1 * r1 - 1.0); + scale_y /= sqrt(abs(r1 * r1 - 1.0)); + } + transform = + mat3(scale_x, 0.0, 0.0, 0.0, scale_y, 0.0, 0.0, 0.0, 1.0) * transform; + + vec2 pt = (transform * vec3(pos.xy, 1.0)).xy; + + // Continue with step 5 onward. + float inv_r1 = 1.0 / r1; + float d_radius_sign = sign(1.0 - f); + bool is_well_behaved = !is_focal_on_circle && r1 > 1.0; + + float x_t = -1.0; + if (is_focal_on_circle) { + x_t = dot(pt, pt) / pt.x; + } else if (is_well_behaved) { + x_t = length(pt) - pt.x * inv_r1; + } else { + float temp = pt.x * pt.x - pt.y * pt.y; + if (temp >= 0.0) { + if (is_swapped || d_radius_sign < 0.0) { + x_t = -sqrt(temp) - pt.x * inv_r1; + } else { + x_t = sqrt(temp) - pt.x * inv_r1; + } + } + } + + if (!is_well_behaved && x_t < 0.0) { + return vec2(0.0, -1.0); + } + + float t = f + d_radius_sign * x_t; + if (is_swapped) { + t = 1.0 - t; } - w -= delta; + return vec2(t, 1.0); } - return 1.0 - result; } /// Compute the indexes and mix coefficient used to mix colors for an diff --git a/impeller/entity/contents/conical_gradient_contents.cc b/impeller/entity/contents/conical_gradient_contents.cc index 2f0412436099f..c40fcff86f6f5 100644 --- a/impeller/entity/contents/conical_gradient_contents.cc +++ b/impeller/entity/contents/conical_gradient_contents.cc @@ -51,18 +51,6 @@ void ConicalGradientContents::SetFocus(std::optional focus, focus_radius_ = radius; } -bool ConicalGradientContents::IsOpaque() const { - if (GetOpacity() < 1) { - return false; - } - for (auto color : colors_) { - if (!color.IsOpaque()) { - return false; - } - } - return true; -} - bool ConicalGradientContents::Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { diff --git a/impeller/entity/contents/conical_gradient_contents.h b/impeller/entity/contents/conical_gradient_contents.h index d773a0e9a2d91..d46ae86a0eabf 100644 --- a/impeller/entity/contents/conical_gradient_contents.h +++ b/impeller/entity/contents/conical_gradient_contents.h @@ -24,9 +24,6 @@ class ConicalGradientContents final : public ColorSourceContents { ~ConicalGradientContents() override; - // |Contents| - bool IsOpaque() const override; - // |Contents| bool Render(const ContentContext& renderer, const Entity& entity, diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index 6d92c3edcaffe..0493d55c5d753 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -2545,7 +2545,7 @@ TEST_P(EntityTest, SolidColorContentsIsOpaque) { TEST_P(EntityTest, ConicalGradientContentsIsOpaque) { ConicalGradientContents contents; contents.SetColors({Color::CornflowerBlue()}); - ASSERT_TRUE(contents.IsOpaque()); + ASSERT_FALSE(contents.IsOpaque()); contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)}); ASSERT_FALSE(contents.IsOpaque()); } diff --git a/impeller/entity/shaders/conical_gradient_fill.frag b/impeller/entity/shaders/conical_gradient_fill.frag index b0400630d7946..a85ae193c5b13 100644 --- a/impeller/entity/shaders/conical_gradient_fill.frag +++ b/impeller/entity/shaders/conical_gradient_fill.frag @@ -25,9 +25,14 @@ highp in vec2 v_position; out vec4 frag_color; void main() { - float t = - IPComputeConicalT(frag_info.center, frag_info.radius, frag_info.focus, - frag_info.focus_radius, v_position); + vec2 res = IPComputeConicalT(frag_info.focus, frag_info.focus_radius, + frag_info.center, frag_info.radius, v_position); + if (res.y < 0.0) { + frag_color = vec4(0); + return; + } + + float t = res.x; frag_color = IPSampleLinearWithTileMode( texture_sampler, vec2(t, 0.5), frag_info.texture_sampler_y_coord_scale, frag_info.half_texel, frag_info.tile_mode); diff --git a/impeller/entity/shaders/conical_gradient_ssbo_fill.frag b/impeller/entity/shaders/conical_gradient_ssbo_fill.frag index c633956b35838..f13fdbe9126f1 100644 --- a/impeller/entity/shaders/conical_gradient_ssbo_fill.frag +++ b/impeller/entity/shaders/conical_gradient_ssbo_fill.frag @@ -32,10 +32,14 @@ highp in vec2 v_position; out vec4 frag_color; void main() { - float t = - IPComputeConicalT(frag_info.center, frag_info.radius, frag_info.focus, - frag_info.focus_radius, v_position); + vec2 res = IPComputeConicalT(frag_info.focus, frag_info.focus_radius, + frag_info.center, frag_info.radius, v_position); + if (res.y < 0.0) { + frag_color = vec4(0); + return; + } + float t = res.x; if ((t < 0.0 || t > 1.0) && frag_info.tile_mode == kTileModeDecal) { frag_color = vec4(0); return; diff --git a/impeller/tools/malioc.json b/impeller/tools/malioc.json index e99ada0edc0a8..aef3c8891d3b6 100644 --- a/impeller/tools/malioc.json +++ b/impeller/tools/malioc.json @@ -1925,16 +1925,17 @@ "has_stack_spilling": false, "performance": { "longest_path_bound_pipelines": [ - null + "arith_total", + "arith_cvt" ], "longest_path_cycles": [ - null, - null, - null, - null, - null, - null, - null + 0.824999988079071, + 0.328125, + 0.824999988079071, + 0.0625, + 0.0, + 0.25, + 0.25 ], "pipelines": [ "arith_total", @@ -1949,10 +1950,10 @@ "varying" ], "shortest_path_cycles": [ - 0.203125, - 0.1875, - 0.203125, - 0.0625, + 0.15625, + 0.0, + 0.15625, + 0.0, 0.0, 0.25, 0.0 @@ -1962,10 +1963,10 @@ "arith_cvt" ], "total_cycles": [ - 0.453125, + 1.125, + 0.699999988079071, + 1.125, 0.3125, - 0.453125, - 0.0625, 0.0, 0.25, 0.25 @@ -1973,8 +1974,8 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 20, - "work_registers_used": 7 + "uniform_registers_used": 38, + "work_registers_used": 9 } } } @@ -1992,7 +1993,7 @@ "uses_late_zs_update": false, "variants": { "Main": { - "fp16_arithmetic": 61, + "fp16_arithmetic": 67, "has_stack_spilling": false, "performance": { "longest_path_bound_pipelines": [ @@ -2020,10 +2021,10 @@ "varying" ], "shortest_path_cycles": [ - 0.1875, - 0.140625, - 0.1875, - 0.0625, + 0.15625, + 0.0, + 0.15625, + 0.0, 0.0, 0.25, 0.0 @@ -2032,10 +2033,10 @@ "load_store" ], "total_cycles": [ - 0.71875, - 0.40625, - 0.71875, - 0.125, + 1.375, + 0.800000011920929, + 1.375, + 0.375, 4.0, 0.25, 0.0 @@ -2043,7 +2044,7 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 18, + "uniform_registers_used": 34, "work_registers_used": 15 } } @@ -6692,20 +6693,21 @@ "uses_late_zs_update": false, "variants": { "Main": { - "fp16_arithmetic": 61, + "fp16_arithmetic": 52, "has_stack_spilling": false, "performance": { "longest_path_bound_pipelines": [ - null + "arith_total", + "arith_cvt" ], "longest_path_cycles": [ - null, - null, - null, - null, - null, - null, - null + 0.925000011920929, + 0.4375, + 0.925000011920929, + 0.1875, + 0.0, + 0.25, + 0.25 ], "pipelines": [ "arith_total", @@ -6717,14 +6719,13 @@ "texture" ], "shortest_path_bound_pipelines": [ - "arith_total", - "arith_cvt" + "varying" ], "shortest_path_cycles": [ - 0.265625, - 0.234375, - 0.265625, - 0.1875, + 0.171875, + 0.0, + 0.171875, + 0.0, 0.0, 0.25, 0.0 @@ -6734,10 +6735,10 @@ "arith_cvt" ], "total_cycles": [ - 0.625, - 0.375, - 0.625, - 0.1875, + 1.2625000476837158, + 0.824999988079071, + 1.2625000476837158, + 0.4375, 0.0, 0.25, 0.25 @@ -6745,8 +6746,8 @@ }, "stack_spill_bytes": 0, "thread_occupancy": 100, - "uniform_registers_used": 14, - "work_registers_used": 20 + "uniform_registers_used": 38, + "work_registers_used": 21 } } }, @@ -6760,12 +6761,12 @@ "has_stack_spilling": false, "performance": { "longest_path_bound_pipelines": [ - null + "arithmetic" ], "longest_path_cycles": [ - null, - null, - null + 12.869999885559082, + 1.0, + 1.0 ], "pipelines": [ "arithmetic", @@ -6776,7 +6777,7 @@ "arithmetic" ], "shortest_path_cycles": [ - 1.9800000190734863, + 1.3200000524520874, 1.0, 0.0 ], @@ -6784,13 +6785,13 @@ "arithmetic" ], "total_cycles": [ - 9.333333015441895, + 19.66666603088379, 1.0, 1.0 ] }, "thread_occupancy": 100, - "uniform_registers_used": 4, + "uniform_registers_used": 6, "work_registers_used": 2 } }