[Impeller] Fix alpha management issues for advanced blends. (#50070)
Resolves https://github.com/flutter/flutter/issues/140970.
The issue is easy to miss in the goldens if you don't know what to look for, but it was always present in one way or another. In the "before" image below, we end up removing some of the backdrop alpha as a result of the blend, which should never happen with an advanced blend:
<img width="171" alt="image" src="https://github.com/flutter/engine/assets/919017/b0ac7c00-fba7-4c86-82c9-94b99ea400e3">
This was happening because we were lerping towards the original source alpha from the destination alpha. So in the sad smiley case, the RGB getting written to the framebuffer was exactly correct, but the alpha would end up being less than 1, which the window manager then blends against the black-initialized framebuffer.
Blending with the black background just so happened to perfectly match the appearance of the color being double-premultiplied, which is why increasing the source color's vibrancy by unpremultiplying also looks correct. But again, that only works iff the pass texture winds up being drawn to a black background, which is a rare case for offscreen passes.
Skia (Flutter documentation):

Impeller before:
<img width="398" alt="image" src="https://github.com/flutter/engine/assets/919017/fda958c5-1bd6-4272-a2b4-f8e25c42cb2f">
Impeller after:
<img width="398" alt="image" src="https://github.com/flutter/engine/assets/919017/a4ca51f8-c287-4e27-9979-44b1c91302b2">
diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc
index b8ace12..a40c1e0 100644
--- a/impeller/aiks/aiks_unittests.cc
+++ b/impeller/aiks/aiks_unittests.cc
@@ -2208,6 +2208,9 @@
Canvas canvas;
canvas.DrawPaint({.color = Color::Black()});
+ // TODO(bdero): Why does this cause the left image to double scale on high DPI
+ // displays.
+ // canvas.Scale(content_scale);
//----------------------------------------------------------------------------
/// 1. Save layer blending (top squares).
@@ -2245,7 +2248,6 @@
canvas.Save();
canvas.Translate({0, 100});
-
// Perform the blend in a SaveLayer so that the initial backdrop color is
// fully transparent black. SourceOver blend the result onto the parent pass.
canvas.SaveLayer({});
@@ -2256,7 +2258,8 @@
.blend_mode = BlendMode::kSourceOver});
canvas.Translate(Vector2(100, 0));
}
- canvas.RestoreToCount(0);
+ canvas.Restore();
+ canvas.Restore();
//----------------------------------------------------------------------------
/// 3. Image blending (bottom images).
@@ -2278,16 +2281,17 @@
}
}
- // Uploaded image source (unpremultiplied source texture).
+ // Uploaded image source (left image).
canvas.Save();
canvas.SaveLayer({.blend_mode = BlendMode::kSourceOver});
{
canvas.DrawImage(dst_image, {0, 0}, {.blend_mode = BlendMode::kSourceOver});
canvas.DrawImage(src_image, {0, 0}, {.blend_mode = blend_mode});
}
- canvas.RestoreToCount(0);
+ canvas.Restore();
+ canvas.Restore();
- // Rendered image source (premultiplied source texture).
+ // Rendered image source (right image).
canvas.Save();
canvas.SaveLayer({.blend_mode = BlendMode::kSourceOver});
{
@@ -2301,7 +2305,7 @@
canvas.Restore();
}
canvas.Restore();
- canvas.RestoreToCount(0);
+ canvas.Restore();
return canvas.EndRecordingAsPicture();
}
@@ -3176,7 +3180,7 @@
});
ASSERT_TRUE(result);
ASSERT_COLOR_NEAR(contents.GetColor(),
- Color(0.433247, 0.879523, 0.825324, 0.75));
+ Color(0.424452, 0.828743, 0.79105, 0.9375));
}
TEST_P(AiksTest, DrawScaledTextWithPerspectiveNoSaveLayer) {
diff --git a/impeller/compiler/shader_lib/impeller/blending.glsl b/impeller/compiler/shader_lib/impeller/blending.glsl
index dc2fd13..59352f2 100644
--- a/impeller/compiler/shader_lib/impeller/blending.glsl
+++ b/impeller/compiler/shader_lib/impeller/blending.glsl
@@ -6,9 +6,28 @@
#define BLENDING_GLSL_
#include <impeller/branching.glsl>
+#include <impeller/color.glsl>
#include <impeller/constants.glsl>
#include <impeller/types.glsl>
+/// Composite a blended color onto the destination.
+/// All three parameters are unpremultiplied. Returns a premultiplied result.
+///
+/// This routine is the same as `ApplyBlendedColor` in
+/// `impeller/geometry/color.cc`.
+f16vec4 IPApplyBlendedColor(f16vec4 dst, f16vec4 src, f16vec3 blend_result) {
+ dst = IPHalfPremultiply(dst);
+ src =
+ // Use the blended color for areas where the source and destination
+ // colors overlap.
+ IPHalfPremultiply(f16vec4(blend_result, src.a * dst.a)) +
+ // Use the original source color for any remaining non-overlapping areas.
+ IPHalfPremultiply(src) * (1.0hf - dst.a);
+
+ // Source-over composite the blended source color atop the destination.
+ return src + dst * (1.0hf - src.a);
+}
+
//------------------------------------------------------------------------------
/// HSV utilities.
///
diff --git a/impeller/entity/shaders/blending/advanced_blend.frag b/impeller/entity/shaders/blending/advanced_blend.frag
index b15c5db..0e7f809 100644
--- a/impeller/entity/shaders/blending/advanced_blend.frag
+++ b/impeller/entity/shaders/blending/advanced_blend.frag
@@ -36,20 +36,20 @@
}
void main() {
- f16vec4 dst_sample = Sample(texture_sampler_dst, // sampler
- v_dst_texture_coords // texture coordinates
- ) *
- blend_info.dst_input_alpha;
-
- f16vec4 dst = dst_sample;
- f16vec4 src = blend_info.color_factor > 0.0hf
- ? blend_info.color
- : Sample(texture_sampler_src, // sampler
- v_src_texture_coords // texture coordinates
- ) *
- blend_info.src_input_alpha;
+ f16vec4 dst =
+ IPHalfUnpremultiply(Sample(texture_sampler_dst, // sampler
+ v_dst_texture_coords // texture coordinates
+ ));
+ dst *= blend_info.dst_input_alpha;
+ f16vec4 src = IPHalfUnpremultiply(
+ blend_info.color_factor > 0.0hf
+ ? blend_info.color
+ : Sample(texture_sampler_src, // sampler
+ v_src_texture_coords // texture coordinates
+ ));
+ src *= blend_info.src_input_alpha;
f16vec3 blend_result = AdvancedBlend(dst.rgb, src.rgb, int(blend_type));
- f16vec4 blended = mix(src, f16vec4(blend_result, src.a), dst.a);
- frag_color = mix(dst_sample, blended, src.a);
+
+ frag_color = IPApplyBlendedColor(dst, src, blend_result);
}
diff --git a/impeller/entity/shaders/blending/framebuffer_blend.frag b/impeller/entity/shaders/blending/framebuffer_blend.frag
index 58cedbd..095d5e5 100644
--- a/impeller/entity/shaders/blending/framebuffer_blend.frag
+++ b/impeller/entity/shaders/blending/framebuffer_blend.frag
@@ -55,13 +55,14 @@
}
void main() {
- f16vec4 dst = f16vec4(ReadDestination());
- f16vec4 src = f16vec4(Sample(texture_sampler_src, // sampler
- v_src_texture_coords // texture coordinates
- )) *
- frag_info.src_input_alpha;
+ f16vec4 dst = IPHalfUnpremultiply(f16vec4(ReadDestination()));
+ f16vec4 src = IPHalfUnpremultiply(
+ f16vec4(Sample(texture_sampler_src, // sampler
+ v_src_texture_coords // texture coordinates
+ )));
+ src.a *= frag_info.src_input_alpha;
f16vec3 blend_result = AdvancedBlend(dst.rgb, src.rgb, int(blend_type));
- f16vec4 blended = mix(src, f16vec4(blend_result, src.a), dst.a);
- frag_color = vec4(mix(dst, blended, src.a));
+
+ frag_color = IPApplyBlendedColor(dst, src, blend_result);
}
diff --git a/impeller/geometry/color.cc b/impeller/geometry/color.cc
index 2efe123..d2565e6 100644
--- a/impeller/geometry/color.cc
+++ b/impeller/geometry/color.cc
@@ -193,30 +193,42 @@
return {color.x, color.y, color.z, alpha};
}
+/// Composite a blended color onto the destination.
+/// All three parameters are unpremultiplied. Returns a premultiplied result.
+///
+/// This routine is the same as `IPApplyBlendedColor` in the Impeller shader
+/// library.
+static constexpr inline Color ApplyBlendedColor(Color dst,
+ Color src,
+ Vector3 blend_result) {
+ dst = dst.Premultiply();
+ src =
+ // Use the blended color for areas where the source and destination
+ // colors overlap.
+ FromRGB(blend_result, src.alpha * dst.alpha).Premultiply() +
+ // Use the original source color for any remaining non-overlapping areas.
+ src.Premultiply() * (1.0f - dst.alpha);
+
+ // Source-over composite the blended source color atop the destination.
+ return src + dst * (1.0f - src.alpha);
+}
+
static constexpr inline Color DoColorBlend(
- Color d,
- Color s,
+ Color dst,
+ Color src,
const std::function<Vector3(Vector3, Vector3)>& blend_rgb_func) {
- d = d.Premultiply();
- s = s.Premultiply();
- const Vector3 rgb = blend_rgb_func(ToRGB(d), ToRGB(s));
- const Color blended = Color::Lerp(s, FromRGB(rgb, d.alpha), d.alpha);
- return Color::Lerp(d, blended, s.alpha).Unpremultiply();
+ const Vector3 blend_result = blend_rgb_func(ToRGB(dst), ToRGB(src));
+ return ApplyBlendedColor(dst, src, blend_result).Unpremultiply();
}
static constexpr inline Color DoColorBlendComponents(
- Color d,
- Color s,
+ Color dst,
+ Color src,
const std::function<Scalar(Scalar, Scalar)>& blend_func) {
- d = d.Premultiply();
- s = s.Premultiply();
- const Color blended = Color::Lerp(s,
- Color(blend_func(d.red, s.red), //
- blend_func(d.green, s.green), //
- blend_func(d.blue, s.blue), //
- d.alpha),
- d.alpha);
- return Color::Lerp(d, blended, s.alpha).Unpremultiply();
+ Vector3 blend_result = Vector3(blend_func(dst.red, src.red), //
+ blend_func(dst.green, src.green), //
+ blend_func(dst.blue, src.blue)); //
+ return ApplyBlendedColor(dst, src, blend_result).Unpremultiply();
}
Color Color::Blend(Color src, BlendMode blend_mode) const {
diff --git a/impeller/geometry/geometry_unittests.cc b/impeller/geometry/geometry_unittests.cc
index 6b7f695..d9df8d1 100644
--- a/impeller/geometry/geometry_unittests.cc
+++ b/impeller/geometry/geometry_unittests.cc
@@ -1431,6 +1431,158 @@
}
}
+struct ColorBlendTestData {
+ static constexpr Color kDestinationColor =
+ Color::CornflowerBlue().WithAlpha(0.75);
+ static constexpr Color kSourceColors[] = {Color::White().WithAlpha(0.75),
+ Color::LimeGreen().WithAlpha(0.75),
+ Color::Black().WithAlpha(0.75)};
+
+ // THIS RESULT TABLE IS GENERATED!
+ //
+ // Uncomment the `GenerateColorBlendResults` test below to print a new table
+ // after making changes to `Color::Blend`.
+ static constexpr Color kExpectedResults
+ [sizeof(kSourceColors)]
+ [static_cast<std::underlying_type_t<BlendMode>>(BlendMode::kLast) + 1] = {
+ {
+ {0, 0, 0, 0}, // Clear
+ {1, 1, 1, 0.75}, // Source
+ {0.392157, 0.584314, 0.929412, 0.75}, // Destination
+ {0.878431, 0.916863, 0.985882, 0.9375}, // SourceOver
+ {0.513726, 0.667451, 0.943529, 0.9375}, // DestinationOver
+ {1, 1, 1, 0.5625}, // SourceIn
+ {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn
+ {1, 1, 1, 0.1875}, // SourceOut
+ {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut
+ {0.848039, 0.896078, 0.982353, 0.75}, // SourceATop
+ {0.544118, 0.688235, 0.947059, 0.75}, // DestinationATop
+ {0.696078, 0.792157, 0.964706, 0.375}, // Xor
+ {1, 1, 1, 1}, // Plus
+ {0.392157, 0.584314, 0.929412, 0.5625}, // Modulate
+ {0.878431, 0.916863, 0.985882, 0.9375}, // Screen
+ {0.74902, 0.916863, 0.985882, 0.9375}, // Overlay
+ {0.513726, 0.667451, 0.943529, 0.9375}, // Darken
+ {0.878431, 0.916863, 0.985882, 0.9375}, // Lighten
+ {0.878431, 0.916863, 0.985882, 0.9375}, // ColorDodge
+ {0.513725, 0.667451, 0.943529, 0.9375}, // ColorBurn
+ {0.878431, 0.916863, 0.985882, 0.9375}, // HardLight
+ {0.654166, 0.775505, 0.964318, 0.9375}, // SoftLight
+ {0.643137, 0.566275, 0.428235, 0.9375}, // Difference
+ {0.643137, 0.566275, 0.428235, 0.9375}, // Exclusion
+ {0.513726, 0.667451, 0.943529, 0.9375}, // Multiply
+ {0.617208, 0.655639, 0.724659, 0.9375}, // Hue
+ {0.617208, 0.655639, 0.724659, 0.9375}, // Saturation
+ {0.617208, 0.655639, 0.724659, 0.9375}, // Color
+ {0.878431, 0.916863, 0.985882, 0.9375}, // Luminosity
+ },
+ {
+ {0, 0, 0, 0}, // Clear
+ {0.196078, 0.803922, 0.196078, 0.75}, // Source
+ {0.392157, 0.584314, 0.929412, 0.75}, // Destination
+ {0.235294, 0.76, 0.342745, 0.9375}, // SourceOver
+ {0.352941, 0.628235, 0.782745, 0.9375}, // DestinationOver
+ {0.196078, 0.803922, 0.196078, 0.5625}, // SourceIn
+ {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn
+ {0.196078, 0.803922, 0.196078, 0.1875}, // SourceOut
+ {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut
+ {0.245098, 0.74902, 0.379412, 0.75}, // SourceATop
+ {0.343137, 0.639216, 0.746078, 0.75}, // DestinationATop
+ {0.294118, 0.694118, 0.562745, 0.375}, // Xor
+ {0.441176, 1, 0.844118, 1}, // Plus
+ {0.0768935, 0.469742, 0.182238, 0.5625}, // Modulate
+ {0.424452, 0.828743, 0.79105, 0.9375}, // Screen
+ {0.209919, 0.779839, 0.757001, 0.9375}, // Overlay
+ {0.235294, 0.628235, 0.342745, 0.9375}, // Darken
+ {0.352941, 0.76, 0.782745, 0.9375}, // Lighten
+ {0.41033, 0.877647, 0.825098, 0.9375}, // ColorDodge
+ {0.117647, 0.567403, 0.609098, 0.9375}, // ColorBurn
+ {0.209919, 0.779839, 0.443783, 0.9375}, // HardLight
+ {0.266006, 0.693915, 0.758818, 0.9375}, // SoftLight
+ {0.235294, 0.409412, 0.665098, 0.9375}, // Difference
+ {0.378316, 0.546897, 0.681707, 0.9375}, // Exclusion
+ {0.163783, 0.559493, 0.334441, 0.9375}, // Multiply
+ {0.266235, 0.748588, 0.373686, 0.9375}, // Hue
+ {0.339345, 0.629787, 0.811502, 0.9375}, // Saturation
+ {0.241247, 0.765953, 0.348698, 0.9375}, // Color
+ {0.346988, 0.622282, 0.776792, 0.9375}, // Luminosity
+ },
+ {
+ {0, 0, 0, 0}, // Clear
+ {0, 0, 0, 0.75}, // Source
+ {0.392157, 0.584314, 0.929412, 0.75}, // Destination
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // SourceOver
+ {0.313726, 0.467451, 0.743529, 0.9375}, // DestinationOver
+ {0, 0, 0, 0.5625}, // SourceIn
+ {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn
+ {0, 0, 0, 0.1875}, // SourceOut
+ {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut
+ {0.0980392, 0.146078, 0.232353, 0.75}, // SourceATop
+ {0.294118, 0.438235, 0.697059, 0.75}, // DestinationATop
+ {0.196078, 0.292157, 0.464706, 0.375}, // Xor
+ {0.294118, 0.438235, 0.697059, 1}, // Plus
+ {0, 0, 0, 0.5625}, // Modulate
+ {0.313726, 0.467451, 0.743529, 0.9375}, // Screen
+ {0.0784314, 0.218039, 0.701176, 0.9375}, // Overlay
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // Darken
+ {0.313726, 0.467451, 0.743529, 0.9375}, // Lighten
+ {0.313726, 0.467451, 0.743529, 0.9375}, // ColorDodge
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // ColorBurn
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // HardLight
+ {0.170704, 0.321716, 0.704166, 0.9375}, // SoftLight
+ {0.313726, 0.467451, 0.743529, 0.9375}, // Difference
+ {0.313726, 0.467451, 0.743529, 0.9375}, // Exclusion
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // Multiply
+ {0.417208, 0.455639, 0.524659, 0.9375}, // Hue
+ {0.417208, 0.455639, 0.524659, 0.9375}, // Saturation
+ {0.417208, 0.455639, 0.524659, 0.9375}, // Color
+ {0.0784314, 0.116863, 0.185882, 0.9375}, // Luminosity
+ },
+ };
+};
+
+/// To print a new ColorBlendTestData::kExpectedResults table, uncomment this
+/// test and run with:
+/// --gtest_filter="GeometryTest.GenerateColorBlendResults"
+/*
+TEST(GeometryTest, GenerateColorBlendResults) {
+ auto& o = std::cout;
+ using BlendT = std::underlying_type_t<BlendMode>;
+ o << "{";
+ for (const auto& source : ColorBlendTestData::kSourceColors) {
+ o << "{";
+ for (BlendT blend_i = 0;
+ blend_i < static_cast<BlendT>(BlendMode::kLast) + 1; blend_i++) {
+ auto blend = static_cast<BlendMode>(blend_i);
+ Color c = ColorBlendTestData::kDestinationColor.Blend(source, blend);
+ o << "{" << c.red << "," << c.green << "," << c.blue << "," << c.alpha
+ << "}, // " << BlendModeToString(blend) << std::endl;
+ }
+ o << "},";
+ }
+ o << "};" << std::endl;
+}
+*/
+
+#define _BLEND_MODE_RESULT_CHECK(blend_mode) \
+ blend_i = static_cast<BlendT>(BlendMode::k##blend_mode); \
+ expected = ColorBlendTestData::kExpectedResults[source_i][blend_i]; \
+ EXPECT_COLOR_NEAR(dst.Blend(src, BlendMode::k##blend_mode), expected);
+
+TEST(GeometryTest, ColorBlendReturnsExpectedResults) {
+ using BlendT = std::underlying_type_t<BlendMode>;
+ Color dst = ColorBlendTestData::kDestinationColor;
+ for (size_t source_i = 0;
+ source_i < sizeof(ColorBlendTestData::kSourceColors) / sizeof(Color);
+ source_i++) {
+ Color src = ColorBlendTestData::kSourceColors[source_i];
+
+ size_t blend_i;
+ Color expected;
+ IMPELLER_FOR_EACH_BLEND_MODE(_BLEND_MODE_RESULT_CHECK)
+ }
+}
+
#define _BLEND_MODE_NAME_CHECK(blend_mode) \
case BlendMode::k##blend_mode: \
ASSERT_STREQ(result, #blend_mode); \
diff --git a/impeller/tools/malioc.json b/impeller/tools/malioc.json
index 377fe50..8cf4183 100644
--- a/impeller/tools/malioc.json
+++ b/impeller/tools/malioc.json
@@ -16,14 +16,14 @@
"has_stack_spilling": false,
"performance": {
"longest_path_bound_pipelines": [
- "varying",
- "texture"
+ "arith_total",
+ "arith_fma"
],
"longest_path_cycles": [
- 0.4375,
- 0.4375,
- 0.015625,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.21875,
+ 0.125,
0.0,
0.5,
0.5
@@ -42,23 +42,23 @@
"arith_fma"
],
"shortest_path_cycles": [
- 0.375,
- 0.375,
- 0.09375,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.234375,
+ 0.125,
0.0,
0.25,
0.25
],
"total_bound_pipelines": [
- "varying",
- "texture"
+ "arith_total",
+ "arith_fma"
],
"total_cycles": [
- 0.4375,
- 0.4375,
- 0.09375,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.296875,
+ 0.125,
0.0,
0.5,
0.5
@@ -67,7 +67,7 @@
"stack_spill_bytes": 0,
"thread_occupancy": 100,
"uniform_registers_used": 10,
- "work_registers_used": 16
+ "work_registers_used": 24
}
}
}
@@ -1427,14 +1427,14 @@
"has_stack_spilling": false,
"performance": {
"longest_path_bound_pipelines": [
- "varying",
- "texture"
+ "arith_total",
+ "arith_fma"
],
"longest_path_cycles": [
- 0.4375,
- 0.4375,
- 0.0625,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.265625,
+ 0.125,
0.0,
0.5,
0.5
@@ -1453,23 +1453,23 @@
"arith_fma"
],
"shortest_path_cycles": [
- 0.375,
- 0.375,
- 0.109375,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.25,
+ 0.125,
0.0,
0.25,
0.25
],
"total_bound_pipelines": [
- "varying",
- "texture"
+ "arith_total",
+ "arith_fma"
],
"total_cycles": [
- 0.4375,
- 0.4375,
- 0.140625,
- 0.0,
+ 0.625,
+ 0.625,
+ 0.34375,
+ 0.125,
0.0,
0.5,
0.5
@@ -1478,7 +1478,7 @@
"stack_spill_bytes": 0,
"thread_occupancy": 100,
"uniform_registers_used": 12,
- "work_registers_used": 24
+ "work_registers_used": 22
}
}
},
@@ -1495,7 +1495,7 @@
"arithmetic"
],
"longest_path_cycles": [
- 2.9700000286102295,
+ 3.630000114440918,
2.0,
2.0
],
@@ -1508,7 +1508,7 @@
"arithmetic"
],
"shortest_path_cycles": [
- 2.640000104904175,
+ 3.299999952316284,
1.0,
1.0
],
@@ -1516,7 +1516,7 @@
"arithmetic"
],
"total_cycles": [
- 3.3333332538604736,
+ 4.0,
2.0,
2.0
]