[impeller] refactored LineContents to make it more testable, added tests (#166035)

issue https://github.com/flutter/flutter/issues/165994

This makes the sample radius properly fixed in pixel coordinates. I
don't know if we want to scale them based on dpi. This tweak also made
it so we no longer have lines that disappear with the setting (width:2,
scale:0.2). We have proper unit tests so we can reason about this stuff
correctly.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [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].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[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
This commit is contained in:
gaaclarke
2025-03-28 12:45:06 -07:00
committed by GitHub
parent cf5212d4bb
commit 751924dbc9
5 changed files with 242 additions and 103 deletions

View File

@@ -125,7 +125,9 @@
"csetjmp": "cpp",
"cfenv": "cpp",
"execution": "cpp",
"print": "cpp"
"print": "cpp",
"source_location": "cpp",
"syncstream": "cpp"
},
"C_Cpp.default.includePath": [
"${default}",

View File

@@ -399,18 +399,38 @@ TEST_P(AiksTest, ScaleExperimentAntialiasLines) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setColor(DlColor::kGreenYellow());
paint.setStrokeWidth(line_width);
builder.DrawPaint(DlPaint(DlColor(0xff111111)));
builder.DrawLine(DlPoint(100, 100), DlPoint(350, 100), paint);
builder.DrawLine(DlPoint(100, 100), DlPoint(350, 150), paint);
{
DlPaint paint;
paint.setColor(DlColor::kGreenYellow());
paint.setStrokeWidth(line_width);
builder.Translate(100, 300);
builder.Scale(scale, scale);
builder.Translate(-100, -300);
builder.DrawLine(DlPoint(100, 300), DlPoint(350, 300), paint);
builder.DrawLine(DlPoint(100, 300), DlPoint(350, 450), paint);
builder.DrawLine(DlPoint(100, 100), DlPoint(350, 100), paint);
builder.DrawLine(DlPoint(100, 100), DlPoint(350, 150), paint);
builder.Save();
builder.Translate(100, 300);
builder.Scale(scale, scale);
builder.Translate(-100, -300);
builder.DrawLine(DlPoint(100, 300), DlPoint(350, 300), paint);
builder.DrawLine(DlPoint(100, 300), DlPoint(350, 450), paint);
builder.Restore();
}
{
DlPaint paint;
paint.setColor(DlColor::kGreenYellow());
paint.setStrokeWidth(2.0);
builder.Save();
builder.Translate(100, 500);
builder.Scale(0.2, 0.2);
builder.Translate(-100, -500);
builder.DrawLine(DlPoint(100, 500), DlPoint(350, 500), paint);
builder.DrawLine(DlPoint(100, 500), DlPoint(350, 650), paint);
builder.Restore();
}
return builder.Build();
};

View File

@@ -24,7 +24,64 @@ using CreateGeometryCallback =
const Geometry* geometry)>;
const int32_t kCurveResolution = 32;
const float kSampleRadius = 1.f;
uint8_t DoubleToUint8(double x) {
return static_cast<uint8_t>(std::clamp(std::round(x * 255.0), 0.0, 255.0));
}
/// See also: CreateGradientTexture
std::shared_ptr<Texture> CreateCurveTexture(
Scalar width,
Scalar radius,
Scalar scale,
const std::shared_ptr<impeller::Context>& context) {
//
impeller::TextureDescriptor texture_descriptor;
texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
texture_descriptor.format = PixelFormat::kR8UNormInt;
texture_descriptor.size = {kCurveResolution, 1};
std::vector<uint8_t> curve_data =
LineContents::CreateCurveData(width, radius, scale);
return CreateTexture(texture_descriptor, curve_data, context, "LineCurve");
}
GeometryResult CreateGeometry(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass,
const Geometry* geometry) {
using PerVertexData = LineVertexShader::PerVertexData;
const LineGeometry* line_geometry =
static_cast<const LineGeometry*>(geometry);
auto& transform = entity.GetTransform();
auto& host_buffer = renderer.GetTransientsBuffer();
size_t count = 4;
fml::Status calculate_status;
BufferView vertex_buffer = host_buffer.Emplace(
count * sizeof(PerVertexData), alignof(PerVertexData),
[line_geometry, &transform, &calculate_status](uint8_t* buffer) {
auto vertices = reinterpret_cast<PerVertexData*>(buffer);
calculate_status = LineContents::CalculatePerVertex(
vertices, line_geometry, transform);
});
if (!calculate_status.ok()) {
return kEmptyResult;
}
return GeometryResult{
.type = PrimitiveType::kTriangleStrip,
.vertex_buffer =
{
.vertex_buffer = vertex_buffer,
.vertex_count = count,
.index_type = IndexType::kNone,
},
.transform = entity.GetShaderTransform(pass),
};
}
struct LineInfo {
Vector3 e0;
@@ -55,99 +112,10 @@ LineInfo CalculateLineInfo(Point p0, Point p1, Scalar width, Scalar radius) {
};
}
uint8_t DoubleToUint8(double x) {
return static_cast<uint8_t>(std::clamp(std::round(x * 255.0), 0.0, 255.0));
}
/// See also: CreateGradientTexture
std::shared_ptr<Texture> CreateCurveTexture(
Scalar width,
Scalar radius,
Scalar scale,
const std::shared_ptr<impeller::Context>& context) {
//
impeller::TextureDescriptor texture_descriptor;
texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
texture_descriptor.format = PixelFormat::kR8UNormInt;
texture_descriptor.size = {kCurveResolution, 1};
std::vector<uint8_t> curve_data;
curve_data.reserve(kCurveResolution);
for (int i = 0; i < kCurveResolution; ++i) {
double norm = (static_cast<double>(i) + 1.0) / 32.0;
double loc = scale * norm * (radius + width / 2.0);
double den = radius * 2.0 + 1.0;
curve_data.push_back(DoubleToUint8(loc / den));
}
return CreateTexture(texture_descriptor, curve_data, context, "LineCurve");
}
GeometryResult CreateGeometry(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass,
const Geometry* geometry) {
using PerVertexData = LineVertexShader::PerVertexData;
const LineGeometry* line_geometry =
static_cast<const LineGeometry*>(geometry);
auto& transform = entity.GetTransform();
Point corners[4];
if (!LineGeometry::ComputeCorners(
corners, transform,
/*extend_endpoints=*/line_geometry->GetCap() != Cap::kButt,
line_geometry->GetP0(), line_geometry->GetP1(),
line_geometry->GetWidth() + kSampleRadius * 2.0)) {
return kEmptyResult;
}
auto& host_buffer = renderer.GetTransientsBuffer();
size_t count = 4;
Scalar scale = entity.GetTransform().GetMaxBasisLengthXY();
LineInfo line_info =
CalculateLineInfo(line_geometry->GetP0(), line_geometry->GetP1(),
line_geometry->GetWidth(), kSampleRadius);
BufferView vertex_buffer = host_buffer.Emplace(
count * sizeof(PerVertexData), alignof(PerVertexData),
[&corners, &line_info](uint8_t* buffer) {
auto vertices = reinterpret_cast<PerVertexData*>(buffer);
for (auto& corner : corners) {
*vertices++ = {
.position = corner,
.e0 = line_info.e0,
.e1 = line_info.e1,
.e2 = line_info.e2,
.e3 = line_info.e3,
};
}
});
std::shared_ptr<Texture> curve_texture = CreateCurveTexture(
line_geometry->GetWidth(), kSampleRadius, scale, renderer.GetContext());
SamplerDescriptor sampler_desc;
sampler_desc.min_filter = MinMagFilter::kLinear;
sampler_desc.mag_filter = MinMagFilter::kLinear;
FS::BindCurve(
pass, curve_texture,
renderer.GetContext()->GetSamplerLibrary()->GetSampler(sampler_desc));
return GeometryResult{
.type = PrimitiveType::kTriangleStrip,
.vertex_buffer =
{
.vertex_buffer = vertex_buffer,
.vertex_count = count,
.index_type = IndexType::kNone,
},
.transform = entity.GetShaderTransform(pass),
};
}
} // namespace
const Scalar LineContents::kSampleRadius = 1.f;
std::unique_ptr<LineContents> LineContents::Make(
std::unique_ptr<LineGeometry> geometry,
Color color) {
@@ -167,6 +135,18 @@ bool LineContents::Render(const ContentContext& renderer,
FS::FragInfo frag_info;
frag_info.color = color_;
Scalar scale = entity.GetTransform().GetMaxBasisLengthXY();
std::shared_ptr<Texture> curve_texture = CreateCurveTexture(
geometry_->GetWidth(), kSampleRadius, scale, renderer.GetContext());
SamplerDescriptor sampler_desc;
sampler_desc.min_filter = MinMagFilter::kLinear;
sampler_desc.mag_filter = MinMagFilter::kLinear;
FS::BindCurve(
pass, curve_texture,
renderer.GetContext()->GetSamplerLibrary()->GetSampler(sampler_desc));
PipelineBuilderCallback pipeline_callback =
[&renderer](ContentContextOptions options) {
return renderer.GetLinePipeline(options);
@@ -188,4 +168,47 @@ std::optional<Rect> LineContents::GetCoverage(const Entity& entity) const {
return geometry_->GetCoverage(entity.GetTransform());
}
std::vector<uint8_t> LineContents::CreateCurveData(Scalar width,
Scalar radius,
Scalar scale) {
std::vector<uint8_t> curve_data;
curve_data.reserve(kCurveResolution);
// More simply written as rise / run:
// double slope = 1.0 / ((radius * 2) / (scale * width + radius));
double slope = (scale * width + radius) / (radius * 2);
for (int i = 0; i < kCurveResolution; ++i) {
double norm =
(static_cast<double>(i)) / static_cast<double>(kCurveResolution - 1);
double scaled = slope * norm;
curve_data.push_back(DoubleToUint8(scaled));
}
return curve_data;
}
fml::Status LineContents::CalculatePerVertex(
LineVertexShader::PerVertexData* per_vertex,
const LineGeometry* geometry,
const Matrix& entity_transform) {
Point corners[4];
if (!LineGeometry::ComputeCorners(
corners, entity_transform,
/*extend_endpoints=*/geometry->GetCap() != Cap::kButt,
geometry->GetP0(), geometry->GetP1(),
geometry->GetWidth() + kSampleRadius * 2.0)) {
return fml::Status(fml::StatusCode::kAborted, "No valid corners");
}
LineInfo line_info = CalculateLineInfo(geometry->GetP0(), geometry->GetP1(),
geometry->GetWidth(), kSampleRadius);
for (auto& corner : corners) {
*per_vertex++ = {
.position = corner,
.e0 = line_info.e0,
.e1 = line_info.e1,
.e2 = line_info.e2,
.e3 = line_info.e3,
};
}
return {};
}
} // namespace impeller

View File

@@ -9,10 +9,21 @@
#include "flutter/impeller/entity/contents/contents.h"
#include "flutter/impeller/entity/geometry/line_geometry.h"
#include "impeller/entity/line.vert.h"
namespace impeller {
class LineContents : public Contents {
public:
static const Scalar kSampleRadius;
static std::vector<uint8_t> CreateCurveData(Scalar width,
Scalar radius,
Scalar scale);
static fml::Status CalculatePerVertex(
LineVertexShader::PerVertexData* per_vertex,
const LineGeometry* geometry,
const Matrix& entity_transform);
static std::unique_ptr<LineContents> Make(
std::unique_ptr<LineGeometry> geometry,
Color color);

View File

@@ -3,11 +3,37 @@
// found in the LICENSE file.
#include "impeller/entity/contents/line_contents.h"
#include <algorithm>
#include "impeller/geometry/geometry_asserts.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
namespace impeller {
namespace testing {
namespace {
float lookup(Scalar x) {
return std::clamp(x, /*lo=*/0.f, /*hi=*/1.f);
}
// This mirrors the function in line.frag.
float CalculateLine(const LineVertexShader::PerVertexData& per_vertex,
Point position) {
Vector3 pos = Vector3(position.x, position.y, 1.0);
Scalar d[4] = {pos.Dot(per_vertex.e0), pos.Dot(per_vertex.e1),
pos.Dot(per_vertex.e2), pos.Dot(per_vertex.e3)};
for (int i = 0; i < 4; ++i) {
if (d[i] < 0.f) {
return 0.0;
}
}
return lookup(std::min(d[0], d[2])) * lookup(std::min(d[1], d[3]));
}
} // namespace
TEST(LineContents, Create) {
Path path;
Scalar width = 5.0f;
@@ -29,5 +55,62 @@ TEST(LineContents, Create) {
}
}
TEST(LineContents, CalculatePerVertex) {
LineVertexShader::PerVertexData per_vertex[4];
auto geometry = std::make_unique<LineGeometry>(
/*p0=*/Point{100, 100}, //
/*p1=*/Point{200, 100}, //
/*width=*/5.f, //
/*cap=*/Cap::kButt);
Matrix transform;
fml::Status status =
LineContents::CalculatePerVertex(per_vertex, geometry.get(), transform);
Scalar offset =
(LineContents::kSampleRadius * 2.0 + geometry->GetWidth()) / 2.f;
ASSERT_TRUE(status.ok());
EXPECT_POINT_NEAR(per_vertex[0].position, Point(100, 100 + offset));
EXPECT_POINT_NEAR(per_vertex[1].position, Point(200, 100 + offset));
EXPECT_POINT_NEAR(per_vertex[2].position, Point(100, 100 - offset));
EXPECT_POINT_NEAR(per_vertex[3].position, Point(200, 100 - offset));
for (int i = 1; i < 4; ++i) {
EXPECT_VECTOR3_NEAR(per_vertex[0].e0, per_vertex[i].e0) << i;
EXPECT_VECTOR3_NEAR(per_vertex[0].e1, per_vertex[i].e1) << i;
EXPECT_VECTOR3_NEAR(per_vertex[0].e2, per_vertex[i].e2) << i;
EXPECT_VECTOR3_NEAR(per_vertex[0].e3, per_vertex[i].e3) << i;
}
EXPECT_EQ(CalculateLine(per_vertex[0], Point(0, 0)), 0.f);
EXPECT_NEAR(CalculateLine(per_vertex[0], Point(150, 100 + offset)), 0.f,
kEhCloseEnough);
EXPECT_NEAR(CalculateLine(per_vertex[0], Point(150, 100 + offset * 0.5)),
0.5f, kEhCloseEnough);
EXPECT_NEAR(CalculateLine(per_vertex[0], Point(150, 100)), 1.f,
kEhCloseEnough);
}
TEST(LineContents, CreateCurveData) {
std::vector<uint8_t> data = LineContents::CreateCurveData(/*width=*/31,
/*radius=*/1,
/*scale=*/1);
EXPECT_EQ(data.size(), 32u);
EXPECT_NEAR(data[0] / 255.f, 0.f, kEhCloseEnough);
EXPECT_NEAR(data[1] / 255.f, 0.5f, 0.02);
EXPECT_NEAR(data[2] / 255.f, 1.f, kEhCloseEnough);
EXPECT_NEAR(data[3] / 255.f, 1.f, kEhCloseEnough);
}
TEST(LineContents, CreateCurveDataScaled) {
std::vector<uint8_t> data = LineContents::CreateCurveData(/*width=*/15.5,
/*radius=*/1,
/*scale=*/2);
EXPECT_EQ(data.size(), 32u);
EXPECT_NEAR(data[0] / 255.f, 0.f, kEhCloseEnough);
EXPECT_NEAR(data[1] / 255.f, 0.5f, 0.02);
EXPECT_NEAR(data[2] / 255.f, 1.f, kEhCloseEnough);
EXPECT_NEAR(data[3] / 255.f, 1.f, kEhCloseEnough);
}
} // namespace testing
} // namespace impeller