Visitar URL original
Support wide gamut colors in gradient_generator's CreateGradientTexture by b-luk · Pull Request #191980 · flutter/flutter · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions engine/src/flutter/impeller/entity/contents/gradient_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,61 @@

namespace impeller {

namespace {

std::vector<uint8_t> ToR32G32B32A32FloatBytes(
const std::vector<Color>& colors) {
static_assert(sizeof(Color) == sizeof(float) * 4,
"Color must be exactly 4 floats with no padding.");
static_assert(std::is_standard_layout_v<Color>,
"Color must be standard layout.");
// `Color` is a standard-layout struct of 4 contiguous 32-bit floats (RGBA),
// directly matching the `kR32G32B32A32Float` pixel format layout.
const auto* bytes = reinterpret_cast<const uint8_t*>(colors.data());
return std::vector<uint8_t>(bytes, bytes + colors.size() * sizeof(Color));
}
Comment thread
b-luk marked this conversation as resolved.
Resolved
Comment thread
b-luk marked this conversation as resolved.
Resolved

std::vector<uint8_t> ToR8G8B8A8Bytes(const std::vector<Color>& colors) {
std::vector<uint8_t> bytes;
bytes.reserve(colors.size() * 4);
for (const auto& color : colors) {
auto converted = color.ToR8G8B8A8();
bytes.push_back(converted[0]);
bytes.push_back(converted[1]);
bytes.push_back(converted[2]);
bytes.push_back(converted[3]);
}
return bytes;
}

} // namespace

std::shared_ptr<Texture> CreateGradientTexture(
const GradientData& gradient_data,
const std::shared_ptr<impeller::Context>& context) {
if (gradient_data.texture_size == 0) {
if (gradient_data.colors.empty()) {
FML_DLOG(ERROR) << "Invalid gradient data.";
return nullptr;
}

impeller::TextureDescriptor texture_descriptor;
texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
texture_descriptor.size = {gradient_data.texture_size, 1};
texture_descriptor.size = ISize(gradient_data.colors.size(), 1);
Comment thread
b-luk marked this conversation as resolved.
Resolved
Comment thread
b-luk marked this conversation as resolved.
Resolved

bool is_wide_gamut =
std::any_of(gradient_data.colors.begin(), gradient_data.colors.end(),
[](const Color& c) { return c.IsWideGamut(); });

std::vector<uint8_t> bytes;
if (is_wide_gamut) {
texture_descriptor.format = PixelFormat::kR32G32B32A32Float;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32bit floats seems like overkill, how big are these? Can we use kR16G16B16A16Float? Or can we somehow plumb the framebuffer's pixel format here so we get the BGRA10XR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the most common case of a 2-color gradient, the texture is 2x1, so the texture is 32 bytes with float32 (compared to 8 bytes with non-wide-gamut uint8s). Most gradients will not be much bigger than this, depending on the number of colors and the stop intervals of the gradient.

The worst case is when it uses kMaxGradientTextureSize of 1024, which happens for gradients with 3+ stops and where there is a very large ratio between the smallest and largest stop interval. I'm not expecting this to be a very common case, but with this worst case the texture is 1024x1 and will be 16KB with float32 (compared to 4KB with non-wide-gamut uint8s).

These sizes don't seem like they'd be a big deal to me, but I don't have a lot to base this on so let me know if it actually is a problem.

From asking Gemini about it, textures are sampled in the shader as float32s regardless of the platform's framebuffer pixel format. So using float32s here preserves full color precision and avoids extra conversions (both on the CPU and GPU) back and forth between different pixel formats.

bytes = ToR32G32B32A32FloatBytes(gradient_data.colors);
} else {
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
bytes = ToR8G8B8A8Bytes(gradient_data.colors);
}

return CreateTexture(texture_descriptor, gradient_data.color_bytes, context,
"Gradient");
return CreateTexture(texture_descriptor, bytes, context, "Gradient");
}

std::vector<StopData> CreateGradientColors(const std::vector<Color>& colors,
Expand Down
8 changes: 8 additions & 0 deletions engine/src/flutter/impeller/geometry/color.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ struct Color {
return {red / alpha, green / alpha, blue / alpha, alpha};
}

/// @brief Returns true if any color channel lies outside the standard
/// [0.0, 1.0] sRGB gamut.
constexpr bool IsWideGamut() const {
return red < -kEhCloseEnough || red > 1.0f + kEhCloseEnough ||
green < -kEhCloseEnough || green > 1.0f + kEhCloseEnough ||
blue < -kEhCloseEnough || blue > 1.0f + kEhCloseEnough;
}
Comment thread
b-luk marked this conversation as resolved.
Resolved

/**
* @brief Return a color that is linearly interpolated between colors a
* and b, according to the value of t.
Expand Down
59 changes: 49 additions & 10 deletions engine/src/flutter/impeller/geometry/geometry_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,19 @@ TEST(GeometryTest, ToIColor) {
ASSERT_EQ(Color::ToIColor(Color(0.5, 0.5, 1.0, 1.0)), 0xFF8080FF);
}

TEST(GeometryTest, ColorIsWideGamut) {
EXPECT_FALSE(Color::Red().IsWideGamut());
EXPECT_FALSE(Color::White().IsWideGamut());
EXPECT_FALSE(Color::BlackTransparent().IsWideGamut());
EXPECT_FALSE(Color(0.5f, 0.5f, 0.5f, 1.0f).IsWideGamut());

// Wide gamut Display P3 values in extended linear sRGB
EXPECT_TRUE(Color(1.0931f, -0.2268f, -0.1501f, 1.0f).IsWideGamut());
EXPECT_TRUE(Color(-0.5116f, 1.0183f, -0.3107f, 1.0f).IsWideGamut());
EXPECT_TRUE(Color(1.5f, 0.5f, 0.5f, 1.0f).IsWideGamut());
EXPECT_TRUE(Color(-0.1f, 0.5f, 0.5f, 1.0f).IsWideGamut());
}
Comment thread
b-luk marked this conversation as resolved.
Resolved

TEST(GeometryTest, Gradient) {
{
// Simple 2 color gradient produces color buffer containing exactly those
Expand All @@ -1829,8 +1842,8 @@ TEST(GeometryTest, Gradient) {

auto gradient = CreateGradientBuffer(colors, stops);

ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors);
ASSERT_EQ(gradient.texture_size, 2u);
EXPECT_COLORS_NEAR(gradient.colors, colors);
EXPECT_EQ(gradient.colors.size(), 2u);
}

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

auto gradient = CreateGradientBuffer(colors, stops);
ASSERT_EQ(gradient.texture_size, 1024u);
EXPECT_EQ(gradient.colors.size(), 1024u);

std::vector<Color> expected_colors(1024);
for (size_t i = 0; i < 1024; i++) {
Expand All @@ -1855,7 +1868,7 @@ TEST(GeometryTest, Gradient) {
Color::Lerp(Color::Black(), Color::Blue(), (t - 0.25) / 0.75);
}
}
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, expected_colors);
EXPECT_COLORS_NEAR(gradient.colors, expected_colors);
}

{
Expand All @@ -1867,8 +1880,8 @@ TEST(GeometryTest, Gradient) {

auto gradient = CreateGradientBuffer(colors, stops);

ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors);
ASSERT_EQ(gradient.texture_size, 4u);
EXPECT_COLORS_NEAR(gradient.colors, colors);
EXPECT_EQ(gradient.colors.size(), 4u);
}

{
Expand All @@ -1885,8 +1898,8 @@ TEST(GeometryTest, Gradient) {
Color::Lerp(Color::Blue(), Color::Green(), 0.6666),
Color::Green(),
};
ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, lerped_colors);
ASSERT_EQ(gradient.texture_size, 5u);
EXPECT_COLORS_NEAR(gradient.colors, lerped_colors);
EXPECT_EQ(gradient.colors.size(), 5u);
}

{
Expand All @@ -1900,8 +1913,34 @@ TEST(GeometryTest, Gradient) {

auto gradient = CreateGradientBuffer(colors, stops);

ASSERT_EQ(gradient.texture_size, 1024u);
ASSERT_EQ(gradient.color_bytes.size(), 1024u * 4);
EXPECT_EQ(gradient.colors.size(), 1024u);
}

{
// Gradient with wide-gamut (out of [0, 1]) colors produces wide-gamut
// values.
Color p3_red = Color(1.0931f, -0.2268f, -0.1501f, 1.0f);
Color p3_green = Color(-0.5116f, 1.0183f, -0.3107f, 1.0f);
std::vector<Color> colors = {p3_red, p3_green};
std::vector<Scalar> stops = {0.0f, 1.0f};

auto gradient = CreateGradientBuffer(colors, stops);

EXPECT_EQ(gradient.colors.size(), 2u);
EXPECT_COLORS_NEAR(gradient.colors, colors);
}

{
// Multi-stop wide-gamut gradient interpolates floats accurately.
Color p3_red = Color(1.0931f, -0.2268f, -0.1501f, 1.0f);
Color p3_green = Color(-0.5116f, 1.0183f, -0.3107f, 1.0f);
std::vector<Color> colors = {p3_red, Color::White(), p3_green};
std::vector<Scalar> stops = {0.0f, 0.5f, 1.0f};

auto gradient = CreateGradientBuffer(colors, stops);

EXPECT_EQ(gradient.colors.size(), 3u);
EXPECT_COLORS_NEAR(gradient.colors, colors);
}
}

Expand Down
33 changes: 10 additions & 23 deletions engine/src/flutter/impeller/geometry/gradient.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "impeller/geometry/gradient.h"

#include <algorithm>

#include "flutter/fml/logging.h"
#include "impeller/geometry/gradient.h"

namespace impeller {

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

static void AppendColor(const Color& color, GradientData* data) {
auto converted = color.ToR8G8B8A8();
data->color_bytes.push_back(converted[0]);
data->color_bytes.push_back(converted[1]);
data->color_bytes.push_back(converted[2]);
data->color_bytes.push_back(converted[3]);
}

GradientData CreateGradientBuffer(const std::vector<Color>& colors,
const std::vector<Scalar>& stops) {
FML_DCHECK(stops.size() == colors.size());
Expand All @@ -45,32 +38,26 @@ GradientData CreateGradientBuffer(const std::vector<Color>& colors,
}
texture_size = static_cast<uint32_t>(std::round(1.0 / minimum_delta)) + 1;
}
GradientData data = {
.color_bytes = {},
.texture_size = texture_size,
};
data.color_bytes.reserve(texture_size * 4);

GradientData result;
if (texture_size == colors.size() &&
colors.size() <= kMaxGradientTextureSize) {
for (auto i = 0u; i < colors.size(); i++) {
AppendColor(colors[i], &data);
}
result.colors = colors;
} else {
result.colors.reserve(texture_size);
Color previous_color = colors[0];
auto previous_stop = 0.0;
auto previous_color_index = 0;

// The first index is always equal to the first color, exactly.
AppendColor(previous_color, &data);
result.colors.push_back(previous_color);

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

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

AppendColor(mixed_color, &data);
result.colors.push_back(mixed_color);
} else {
// We've passed the next stop. Advance to the next stop interval.
// Decrement `i` to re-evaluate the current texel against the new
Expand All @@ -93,9 +80,9 @@ GradientData CreateGradientBuffer(const std::vector<Color>& colors,
}
}
// The last index is always equal to the last color, exactly.
AppendColor(colors.back(), &data);
result.colors.push_back(colors.back());
}
return data;
return result;
}

} // namespace impeller
7 changes: 3 additions & 4 deletions engine/src/flutter/impeller/geometry/gradient.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@

namespace impeller {

// If texture_size is 0 then the gradient is invalid.
// If colors is empty then the gradient is invalid.
struct GradientData {
std::vector<uint8_t> color_bytes;
uint32_t texture_size;
std::vector<Color> colors;
};

/**
* @brief Populate a vector with the interpolated color bytes for the linear
* @brief Populate GradientData with the interpolated colors for the linear
* gradient described by colors and stops.
*
* @param colors
Expand Down