Sitelet https://github.com/3M-com/flutter/commit/1d790af2e336b3a4707c81b2117da194d359db99
Skip to content

Commit 1d790af

Browse files
authored
Support wide gamut colors in gradient_generator's CreateGradientTexture (flutter#191980)
This is a prerequisite to re-implementing flutter#190874. This addresses the unsupported wide gamut issue which was originally present in that PR and which caused it to be reverted. Related: flutter#190401 ### Previously: - `CreateGradientBuffer` converts a gradient's colors and stops to an interpolated list of colors. This interpolated list of colors is converted to bytes with `ToR8G8B8A8()` and is returned in a `GradientData`. - The `R8G8B8A8` `GradientData` is passed to `CreateGradientTexture`, which allocates a `PixelFormat::kR8G8B8A8UNormInt` texture. Wide gamut colors can't be converted to `R8G8B8A8`, so they end up getting clamped. ### Now: - `CreateGradientBuffer` converts a gradient's colors and stops to an interpolated list of colors (same as before). But now no conversion to bytes happens here. The interpolated list of colors is directly returned in a `GradientData`. - The `Color`-based `GradientData` is passed to `CreateGradientTexture`. `CreateGradientTexture` now handles the conversion to bytes. If there are wide gamut colors, it converts to `kR32G32B32A32Float`. Otherwise it does the old behavior of converting to `PixelFormat::kR8G8B8A8UNormInt`. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 5474d44 commit 1d790af

5 files changed

Lines changed: 115 additions & 42 deletions

File tree

‎engine/src/flutter/impeller/entity/contents/gradient_generator.cc‎

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,61 @@
1414

1515
namespace impeller {
1616

17+
namespace {
18+
19+
std::vector<uint8_t> ToR32G32B32A32FloatBytes(
20+
const std::vector<Color>& colors) {
21+
static_assert(sizeof(Color) == sizeof(float) * 4,
22+
"Color must be exactly 4 floats with no padding.");
23+
static_assert(std::is_standard_layout_v<Color>,
24+
"Color must be standard layout.");
25+
// `Color` is a standard-layout struct of 4 contiguous 32-bit floats (RGBA),
26+
// directly matching the `kR32G32B32A32Float` pixel format layout.
27+
const auto* bytes = reinterpret_cast<const uint8_t*>(colors.data());
28+
return std::vector<uint8_t>(bytes, bytes + colors.size() * sizeof(Color));
29+
}
30+
31+
std::vector<uint8_t> ToR8G8B8A8Bytes(const std::vector<Color>& colors) {
32+
std::vector<uint8_t> bytes;
33+
bytes.reserve(colors.size() * 4);
34+
for (const auto& color : colors) {
35+
auto converted = color.ToR8G8B8A8();
36+
bytes.push_back(converted[0]);
37+
bytes.push_back(converted[1]);
38+
bytes.push_back(converted[2]);
39+
bytes.push_back(converted[3]);
40+
}
41+
return bytes;
42+
}
43+
44+
} // namespace
45+
1746
std::shared_ptr<Texture> CreateGradientTexture(
1847
const GradientData& gradient_data,
1948
const std::shared_ptr<impeller::Context>& context) {
20-
if (gradient_data.texture_size == 0) {
49+
if (gradient_data.colors.empty()) {
2150
FML_DLOG(ERROR) << "Invalid gradient data.";
2251
return nullptr;
2352
}
2453

2554
impeller::TextureDescriptor texture_descriptor;
2655
texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
27-
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
28-
texture_descriptor.size = {gradient_data.texture_size, 1};
56+
texture_descriptor.size = ISize(gradient_data.colors.size(), 1);
57+
58+
bool is_wide_gamut =
59+
std::any_of(gradient_data.colors.begin(), gradient_data.colors.end(),
60+
[](const Color& c) { return c.IsWideGamut(); });
61+
62+
std::vector<uint8_t> bytes;
63+
if (is_wide_gamut) {
64+
texture_descriptor.format = PixelFormat::kR32G32B32A32Float;
65+
bytes = ToR32G32B32A32FloatBytes(gradient_data.colors);
66+
} else {
67+
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
68+
bytes = ToR8G8B8A8Bytes(gradient_data.colors);
69+
}
2970

30-
return CreateTexture(texture_descriptor, gradient_data.color_bytes, context,
31-
"Gradient");
71+
return CreateTexture(texture_descriptor, bytes, context, "Gradient");
3272
}
3373

3474
std::vector<StopData> CreateGradientColors(const std::vector<Color>& colors,

‎engine/src/flutter/impeller/geometry/color.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ struct Color {
220220
return {red / alpha, green / alpha, blue / alpha, alpha};
221221
}
222222

223+
/// @brief Returns true if any color channel lies outside the standard
224+
/// [0.0, 1.0] sRGB gamut.
225+
constexpr bool IsWideGamut() const {
226+
return red < -kEhCloseEnough || red > 1.0f + kEhCloseEnough ||
227+
green < -kEhCloseEnough || green > 1.0f + kEhCloseEnough ||
228+
blue < -kEhCloseEnough || blue > 1.0f + kEhCloseEnough;
229+
}
230+
223231
/**
224232
* @brief Return a color that is linearly interpolated between colors a
225233
* and b, according to the value of t.

‎engine/src/flutter/impeller/geometry/geometry_unittests.cc‎

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,19 @@ TEST(GeometryTest, ToIColor) {
18201820
ASSERT_EQ(Color::ToIColor(Color(0.5, 0.5, 1.0, 1.0)), 0xFF8080FF);
18211821
}
18221822

1823+
TEST(GeometryTest, ColorIsWideGamut) {
1824+
EXPECT_FALSE(Color::Red().IsWideGamut());
1825+
EXPECT_FALSE(Color::White().IsWideGamut());
1826+
EXPECT_FALSE(Color::BlackTransparent().IsWideGamut());
1827+
EXPECT_FALSE(Color(0.5f, 0.5f, 0.5f, 1.0f).IsWideGamut());
1828+
1829+
// Wide gamut Display P3 values in extended linear sRGB
1830+
EXPECT_TRUE(Color(1.0931f, -0.2268f, -0.1501f, 1.0f).IsWideGamut());
1831+
EXPECT_TRUE(Color(-0.5116f, 1.0183f, -0.3107f, 1.0f).IsWideGamut());
1832+
EXPECT_TRUE(Color(1.5f, 0.5f, 0.5f, 1.0f).IsWideGamut());
1833+
EXPECT_TRUE(Color(-0.1f, 0.5f, 0.5f, 1.0f).IsWideGamut());
1834+
}
1835+
18231836
TEST(GeometryTest, Gradient) {
18241837
{
18251838
// Simple 2 color gradient produces color buffer containing exactly those
@@ -1829,8 +1842,8 @@ TEST(GeometryTest, Gradient) {
18291842

18301843
auto gradient = CreateGradientBuffer(colors, stops);
18311844

1832-
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors);
1833-
ASSERT_EQ(gradient.texture_size, 2u);
1845+
EXPECT_COLORS_NEAR(gradient.colors, colors);
1846+
EXPECT_EQ(gradient.colors.size(), 2u);
18341847
}
18351848

18361849
{
@@ -1840,7 +1853,7 @@ TEST(GeometryTest, Gradient) {
18401853
std::vector<Scalar> stops = {0.0, 0.25, 0.25, 1.0};
18411854

18421855
auto gradient = CreateGradientBuffer(colors, stops);
1843-
ASSERT_EQ(gradient.texture_size, 1024u);
1856+
EXPECT_EQ(gradient.colors.size(), 1024u);
18441857

18451858
std::vector<Color> expected_colors(1024);
18461859
for (size_t i = 0; i < 1024; i++) {
@@ -1855,7 +1868,7 @@ TEST(GeometryTest, Gradient) {
18551868
Color::Lerp(Color::Black(), Color::Blue(), (t - 0.25) / 0.75);
18561869
}
18571870
}
1858-
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, expected_colors);
1871+
EXPECT_COLORS_NEAR(gradient.colors, expected_colors);
18591872
}
18601873

18611874
{
@@ -1867,8 +1880,8 @@ TEST(GeometryTest, Gradient) {
18671880

18681881
auto gradient = CreateGradientBuffer(colors, stops);
18691882

1870-
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors);
1871-
ASSERT_EQ(gradient.texture_size, 4u);
1883+
EXPECT_COLORS_NEAR(gradient.colors, colors);
1884+
EXPECT_EQ(gradient.colors.size(), 4u);
18721885
}
18731886

18741887
{
@@ -1885,8 +1898,8 @@ TEST(GeometryTest, Gradient) {
18851898
Color::Lerp(Color::Blue(), Color::Green(), 0.6666),
18861899
Color::Green(),
18871900
};
1888-
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, lerped_colors);
1889-
ASSERT_EQ(gradient.texture_size, 5u);
1901+
EXPECT_COLORS_NEAR(gradient.colors, lerped_colors);
1902+
EXPECT_EQ(gradient.colors.size(), 5u);
18901903
}
18911904

18921905
{
@@ -1900,8 +1913,34 @@ TEST(GeometryTest, Gradient) {
19001913

19011914
auto gradient = CreateGradientBuffer(colors, stops);
19021915

1903-
ASSERT_EQ(gradient.texture_size, 1024u);
1904-
ASSERT_EQ(gradient.color_bytes.size(), 1024u * 4);
1916+
EXPECT_EQ(gradient.colors.size(), 1024u);
1917+
}
1918+
1919+
{
1920+
// Gradient with wide-gamut (out of [0, 1]) colors produces wide-gamut
1921+
// values.
1922+
Color p3_red = Color(1.0931f, -0.2268f, -0.1501f, 1.0f);
1923+
Color p3_green = Color(-0.5116f, 1.0183f, -0.3107f, 1.0f);
1924+
std::vector<Color> colors = {p3_red, p3_green};
1925+
std::vector<Scalar> stops = {0.0f, 1.0f};
1926+
1927+
auto gradient = CreateGradientBuffer(colors, stops);
1928+
1929+
EXPECT_EQ(gradient.colors.size(), 2u);
1930+
EXPECT_COLORS_NEAR(gradient.colors, colors);
1931+
}
1932+
1933+
{
1934+
// Multi-stop wide-gamut gradient interpolates floats accurately.
1935+
Color p3_red = Color(1.0931f, -0.2268f, -0.1501f, 1.0f);
1936+
Color p3_green = Color(-0.5116f, 1.0183f, -0.3107f, 1.0f);
1937+
std::vector<Color> colors = {p3_red, Color::White(), p3_green};
1938+
std::vector<Scalar> stops = {0.0f, 0.5f, 1.0f};
1939+
1940+
auto gradient = CreateGradientBuffer(colors, stops);
1941+
1942+
EXPECT_EQ(gradient.colors.size(), 3u);
1943+
EXPECT_COLORS_NEAR(gradient.colors, colors);
19051944
}
19061945
}
19071946

‎engine/src/flutter/impeller/geometry/gradient.cc‎

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,18 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
#include "impeller/geometry/gradient.h"
6+
57
#include <algorithm>
68

79
#include "flutter/fml/logging.h"
8-
#include "impeller/geometry/gradient.h"
910

1011
namespace impeller {
1112

1213
// TODO(b-luk): this should use a platform specific max texture size.
1314
// https://github.com/flutter/flutter/issues/191820
1415
static constexpr uint32_t kMaxGradientTextureSize = 1024;
1516

16-
static void AppendColor(const Color& color, GradientData* data) {
17-
auto converted = color.ToR8G8B8A8();
18-
data->color_bytes.push_back(converted[0]);
19-
data->color_bytes.push_back(converted[1]);
20-
data->color_bytes.push_back(converted[2]);
21-
data->color_bytes.push_back(converted[3]);
22-
}
23-
2417
GradientData CreateGradientBuffer(const std::vector<Color>& colors,
2518
const std::vector<Scalar>& stops) {
2619
FML_DCHECK(stops.size() == colors.size());
@@ -45,32 +38,26 @@ GradientData CreateGradientBuffer(const std::vector<Color>& colors,
4538
}
4639
texture_size = static_cast<uint32_t>(std::round(1.0 / minimum_delta)) + 1;
4740
}
48-
GradientData data = {
49-
.color_bytes = {},
50-
.texture_size = texture_size,
51-
};
52-
data.color_bytes.reserve(texture_size * 4);
53-
41+
GradientData result;
5442
if (texture_size == colors.size() &&
5543
colors.size() <= kMaxGradientTextureSize) {
56-
for (auto i = 0u; i < colors.size(); i++) {
57-
AppendColor(colors[i], &data);
58-
}
44+
result.colors = colors;
5945
} else {
46+
result.colors.reserve(texture_size);
6047
Color previous_color = colors[0];
6148
auto previous_stop = 0.0;
6249
auto previous_color_index = 0;
6350

6451
// The first index is always equal to the first color, exactly.
65-
AppendColor(previous_color, &data);
52+
result.colors.push_back(previous_color);
6653

6754
for (auto i = 1u; i < texture_size - 1; i++) {
6855
auto scaled_i = i / (texture_size - 1.0);
6956
Color next_color = colors[previous_color_index + 1];
7057
auto next_stop = stops[previous_color_index + 1];
7158
// We're almost exactly equal to the next stop.
7259
if (ScalarNearlyEqual(scaled_i, next_stop)) {
73-
AppendColor(next_color, &data);
60+
result.colors.push_back(next_color);
7461

7562
previous_color = next_color;
7663
previous_stop = next_stop;
@@ -80,7 +67,7 @@ GradientData CreateGradientBuffer(const std::vector<Color>& colors,
8067
auto t = (scaled_i - previous_stop) / (next_stop - previous_stop);
8168
auto mixed_color = Color::Lerp(previous_color, next_color, t);
8269

83-
AppendColor(mixed_color, &data);
70+
result.colors.push_back(mixed_color);
8471
} else {
8572
// We've passed the next stop. Advance to the next stop interval.
8673
// Decrement `i` to re-evaluate the current texel against the new
@@ -93,9 +80,9 @@ GradientData CreateGradientBuffer(const std::vector<Color>& colors,
9380
}
9481
}
9582
// The last index is always equal to the last color, exactly.
96-
AppendColor(colors.back(), &data);
83+
result.colors.push_back(colors.back());
9784
}
98-
return data;
85+
return result;
9986
}
10087

10188
} // namespace impeller

‎engine/src/flutter/impeller/geometry/gradient.h‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212

1313
namespace impeller {
1414

15-
// If texture_size is 0 then the gradient is invalid.
15+
// If colors is empty then the gradient is invalid.
1616
struct GradientData {
17-
std::vector<uint8_t> color_bytes;
18-
uint32_t texture_size;
17+
std::vector<Color> colors;
1918
};
2019

2120
/**
22-
* @brief Populate a vector with the interpolated color bytes for the linear
21+
* @brief Populate GradientData with the interpolated colors for the linear
2322
* gradient described by colors and stops.
2423
*
2524
* @param colors

0 commit comments

Comments
 (0)