Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Started pixel aligning hairlines #166351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 2, 2025
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
10 changes: 10 additions & 0 deletions engine/src/flutter/display_list/geometry/dl_path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ bool DlPath::IsOval(DlRect* bounds) const {
return GetSkPath().isOval(ToSkRect(bounds));
}

bool DlPath::IsLine(DlPoint* start, DlPoint* end) const {
SkPoint sk_points[2];
if (GetSkPath().isLine(sk_points)) {
*start = ToDlPoint(sk_points[0]);
*end = ToDlPoint(sk_points[1]);
return true;
}
return false;
}

bool DlPath::IsRoundRect(DlRoundRect* rrect) const {
SkRRect sk_rrect;
bool ret = GetSkPath().isRRect(rrect ? &sk_rrect : nullptr);
Expand Down
1 change: 1 addition & 0 deletions engine/src/flutter/display_list/geometry/dl_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class DlPath {

bool IsRect(DlRect* rect = nullptr, bool* is_closed = nullptr) const;
bool IsOval(DlRect* bounds = nullptr) const;
bool IsLine(DlPoint* start = nullptr, DlPoint* end = nullptr) const;
bool IsRoundRect(DlRoundRect* rrect = nullptr) const;

bool IsSkRect(SkRect* rect, bool* is_closed = nullptr) const;
Expand Down
39 changes: 39 additions & 0 deletions engine/src/flutter/display_list/geometry/dl_path_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,45 @@ TEST(DisplayListPath, ConstructFromImpellerEqualsConstructFromSkia) {
EXPECT_EQ(DlPath(path_builder, DlPathFillType::kNonZero), DlPath(sk_path));
}

TEST(DisplayListPath, IsLineFromSkPath) {
SkPath sk_path;
sk_path.moveTo(SkPoint::Make(0, 0));
sk_path.lineTo(SkPoint::Make(100, 100));

DlPath path = DlPath(sk_path);

DlPoint start;
DlPoint end;
EXPECT_TRUE(path.IsLine(&start, &end));
EXPECT_EQ(start, DlPoint::MakeXY(0, 0));
EXPECT_EQ(end, DlPoint::MakeXY(100, 100));

EXPECT_FALSE(DlPath(SkPath::Rect(SkRect::MakeLTRB(0, 0, 100, 100))).IsLine());
}

TEST(DisplayListPath, IsLineFromImpellerPath) {
DlPathBuilder path_builder;
path_builder.MoveTo({0, 0});
path_builder.LineTo({100, 0});
DlPath path = DlPath(path_builder, DlPathFillType::kNonZero);

DlPoint start;
DlPoint end;
EXPECT_TRUE(path.IsLine(&start, &end));
EXPECT_EQ(start, DlPoint::MakeXY(0, 0));
EXPECT_EQ(end, DlPoint::MakeXY(100, 0));

{
DlPathBuilder path_builder;
path_builder.MoveTo({0, 0});
path_builder.LineTo({100, 0});
path_builder.LineTo({100, 100});

DlPath path = DlPath(path_builder, DlPathFillType::kNonZero);
EXPECT_FALSE(path.IsLine());
}
}

namespace {
class DlPathReceiverMock : public DlPathReceiver {
public:
Expand Down
82 changes: 82 additions & 0 deletions engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,88 @@ TEST_P(AiksTest, CanRenderStrokedConicPaths) {
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}

TEST_P(AiksTest, HairlinePath) {
Scalar scale = 1.f;
Scalar rotation = 0.f;
Scalar offset = 0.f;
auto callback = [&]() -> sk_sp<DisplayList> {
if (AiksTest::ImGuiBegin("Controls", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SliderFloat("Scale", &scale, 0, 6);
ImGui::SliderFloat("Rotate", &rotation, 0, 90);
ImGui::SliderFloat("Offset", &offset, 0, 2);
ImGui::End();
}

DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
builder.DrawPaint(DlPaint(DlColor(0xff111111)));

DlPaint paint;
paint.setStrokeWidth(0.f);
paint.setColor(DlColor::kWhite());
paint.setStrokeCap(DlStrokeCap::kRound);
paint.setStrokeJoin(DlStrokeJoin::kRound);
paint.setDrawStyle(DlDrawStyle::kStroke);

builder.Translate(512, 384);
builder.Scale(scale, scale);
builder.Rotate(rotation);
builder.Translate(-512, -384 + offset);

for (int i = 0; i < 5; ++i) {
Scalar yoffset = i * 25.25f + 300.f;
DlPathBuilder path_builder;

path_builder.MoveTo(DlPoint(100, yoffset));
path_builder.LineTo(DlPoint(924, yoffset));
builder.DrawPath(DlPath(path_builder), paint);
}

return builder.Build();
};

ASSERT_TRUE(OpenPlaygroundHere(callback));
}

TEST_P(AiksTest, HairlineDrawLine) {
Scalar scale = 1.f;
Scalar rotation = 0.f;
Scalar offset = 0.f;
auto callback = [&]() -> sk_sp<DisplayList> {
if (AiksTest::ImGuiBegin("Controls", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SliderFloat("Scale", &scale, 0, 6);
ImGui::SliderFloat("Rotate", &rotation, 0, 90);
ImGui::SliderFloat("Offset", &offset, 0, 2);
ImGui::End();
}

DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
builder.DrawPaint(DlPaint(DlColor(0xff111111)));

DlPaint paint;
paint.setStrokeWidth(0.f);
paint.setColor(DlColor::kWhite());

builder.Translate(512, 384);
builder.Scale(scale, scale);
builder.Rotate(rotation);
builder.Translate(-512, -384 + offset);

for (int i = 0; i < 5; ++i) {
Scalar yoffset = i * 25.25f + 300.f;

builder.DrawLine(DlPoint(100, yoffset), DlPoint(924, yoffset), paint);
}

return builder.Build();
};

ASSERT_TRUE(OpenPlaygroundHere(callback));
}

TEST_P(AiksTest, CanRenderTightConicPath) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
Expand Down
7 changes: 7 additions & 0 deletions engine/src/flutter/impeller/display_list/dl_dispatcher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,13 @@ void DlDispatcherBase::SimplifyOrDrawPath(Canvas& canvas,
return;
}

DlPoint start;
DlPoint end;
if (path.IsLine(&start, &end)) {
canvas.DrawLine(start, end, paint);
return;
}

canvas.DrawPath(path.GetPath(), paint);
}

Expand Down
40 changes: 35 additions & 5 deletions engine/src/flutter/impeller/entity/geometry/line_geometry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,52 @@ Scalar LineGeometry::ComputeAlphaCoverage(const Matrix& entity) const {
return Geometry::ComputeStrokeAlphaCoverage(entity, width_);
}

namespace {
/// Minimizes the err when rounding to the closest 0.5 value.
/// If we round up, it drops down a half. If we round down it bumps up a half.
Scalar RoundToHalf(Scalar x) {
Scalar whole;
std::modf(x, &whole);
return whole + 0.5;
}
} // namespace

GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) const {
using VT = SolidFillVertexShader::PerVertexData;

auto& transform = entity.GetTransform();
Matrix transform = entity.GetTransform();
auto radius = ComputePixelHalfWidth(transform, width_);

Point p0 = p0_;
Point p1 = p1_;

// Hairline pixel alignment.
if (width_ == 0.f && transform.IsTranslationScaleOnly()) {
p0 = transform * p0_;
p1 = transform * p1_;
transform = Matrix();
if (std::fabs(p0.x - p1.x) < kEhCloseEnough) {
p0.x = RoundToHalf(p0.x);
p1.x = p0.x;
} else if (std::fabs(p0.y - p1.y) < kEhCloseEnough) {
p0.y = RoundToHalf(p0.y);
p1.y = p0.y;
}
}

Entity fixed_transform = entity.Clone();
fixed_transform.SetTransform(transform);

if (cap_ == Cap::kRound) {
auto generator =
renderer.GetTessellator().RoundCapLine(transform, p0_, p1_, radius);
return ComputePositionGeometry(renderer, generator, entity, pass);
renderer.GetTessellator().RoundCapLine(transform, p0, p1, radius);
return ComputePositionGeometry(renderer, generator, fixed_transform, pass);
}

Point corners[4];
if (!ComputeCorners(corners, transform, cap_ == Cap::kSquare, p0_, p1_,
if (!ComputeCorners(corners, transform, cap_ == Cap::kSquare, p0, p1,
width_)) {
return kEmptyResult;
}
Expand All @@ -119,7 +149,7 @@ GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer,
.vertex_count = count,
.index_type = IndexType::kNone,
},
.transform = entity.GetShaderTransform(pass),
.transform = fixed_transform.GetShaderTransform(pass),
};
}

Expand Down