Skip to content

Commit

Permalink
[Impeller] implement Canvas::DrawLine to tesselate lines directly (fl…
Browse files Browse the repository at this point in the history
…utter#47846)

Impeller implements the DrawLine primitive as DrawPath on a path containing a single line. Benchmarks show that this can cost 30% overhead on apps that use a lot of DrawLine primitives. This PR creates a more direct Entity that can tesselate the geometry of a line directly.

The reduced overhead should help with flutter/flutter#138004

The current code will back off to Path rendering for round caps. When the circle geometry work is finished (flutter#47845) we can come back and implement round caps using the code refactored in that PR.
  • Loading branch information
flar authored Nov 10, 2023
1 parent 68ce6bf commit 77a12e4
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 12 deletions.
4 changes: 4 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,8 @@ ORIGIN: ../../../flutter/impeller/entity/geometry/fill_path_geometry.cc + ../../
ORIGIN: ../../../flutter/impeller/entity/geometry/fill_path_geometry.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/geometry.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/geometry.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/line_geometry.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/line_geometry.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/point_field_geometry.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/point_field_geometry.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/entity/geometry/rect_geometry.cc + ../../../flutter/LICENSE
Expand Down Expand Up @@ -5881,6 +5883,8 @@ FILE: ../../../flutter/impeller/entity/geometry/fill_path_geometry.cc
FILE: ../../../flutter/impeller/entity/geometry/fill_path_geometry.h
FILE: ../../../flutter/impeller/entity/geometry/geometry.cc
FILE: ../../../flutter/impeller/entity/geometry/geometry.h
FILE: ../../../flutter/impeller/entity/geometry/line_geometry.cc
FILE: ../../../flutter/impeller/entity/geometry/line_geometry.h
FILE: ../../../flutter/impeller/entity/geometry/point_field_geometry.cc
FILE: ../../../flutter/impeller/entity/geometry/point_field_geometry.h
FILE: ../../../flutter/impeller/entity/geometry/rect_geometry.cc
Expand Down
64 changes: 64 additions & 0 deletions impeller/aiks/aiks_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,70 @@ TEST_P(AiksTest, SolidStrokesRenderCorrectly) {
ASSERT_TRUE(OpenPlaygroundHere(callback));
}

TEST_P(AiksTest, DrawLinesRenderCorrectly) {
Canvas canvas;
canvas.Scale(GetContentScale());
Paint paint;
paint.color = Color::Blue();
paint.stroke_width = 10;

auto draw = [&canvas](Paint& paint) {
for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) {
paint.stroke_cap = cap;
Point origin = {100, 100};
Point p0 = {50, 0};
Point p1 = {150, 0};
canvas.DrawLine({150, 100}, {250, 100}, paint);
for (int d = 15; d < 90; d += 15) {
Matrix m = Matrix::MakeRotationZ(Degrees(d));
canvas.DrawLine(origin + m * p0, origin + m * p1, paint);
}
canvas.DrawLine({100, 150}, {100, 250}, paint);
canvas.DrawCircle({origin}, 35, paint);

canvas.DrawLine({250, 250}, {250, 250}, paint);

canvas.Translate({250, 0});
}
canvas.Translate({-750, 250});
};

std::vector<Color> colors = {
Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0},
Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0},
Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0},
Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0},
Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0},
Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0},
Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}};
std::vector<Scalar> stops = {
0.0,
(1.0 / 6.0) * 1,
(1.0 / 6.0) * 2,
(1.0 / 6.0) * 3,
(1.0 / 6.0) * 4,
(1.0 / 6.0) * 5,
1.0,
};

auto texture = CreateTextureForFixture("airplane.jpg",
/*enable_mipmapping=*/true);

draw(paint);

paint.color_source = ColorSource::MakeRadialGradient(
{100, 100}, 200, std::move(colors), std::move(stops),
Entity::TileMode::kMirror, {});
draw(paint);

paint.color_source = ColorSource::MakeImage(
texture, Entity::TileMode::kRepeat, Entity::TileMode::kRepeat, {},
Matrix::MakeTranslation({-150, 75}));
draw(paint);

ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, GradientStrokesRenderCorrectly) {
// Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2
auto callback = [&](AiksContext& renderer) -> std::optional<Picture> {
Expand Down
22 changes: 22 additions & 0 deletions impeller/aiks/canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,28 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect,
return true;
}

void Canvas::DrawLine(const Point& p0, const Point& p1, const Paint& paint) {
if (paint.stroke_cap == Cap::kRound) {
auto path = PathBuilder{}
.AddLine((p0), (p1))
.SetConvexity(Convexity::kConvex)
.TakePath();
Paint stroke_paint = paint;
stroke_paint.style = Paint::Style::kStroke;
DrawPath(path, stroke_paint);
return;
}

Entity entity;
entity.SetTransformation(GetCurrentTransformation());
entity.SetClipDepth(GetClipDepth());
entity.SetBlendMode(paint.blend_mode);
entity.SetContents(paint.WithFilters(paint.CreateContentsForGeometry(
Geometry::MakeLine(p0, p1, paint.stroke_width, paint.stroke_cap))));

GetCurrentPass().AddEntity(entity);
}

void Canvas::DrawRect(Rect rect, const Paint& paint) {
if (paint.style == Paint::Style::kStroke) {
DrawPath(PathBuilder{}.AddRect(rect).TakePath(), paint);
Expand Down
2 changes: 2 additions & 0 deletions impeller/aiks/canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class Canvas {

void DrawPaint(const Paint& paint);

void DrawLine(const Point& p0, const Point& p1, const Paint& paint);

void DrawRect(Rect rect, const Paint& paint);

void DrawRRect(Rect rect, Point corner_radii, const Paint& paint);
Expand Down
16 changes: 4 additions & 12 deletions impeller/display_list/dl_dispatcher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -759,14 +759,8 @@ void DlDispatcher::drawPaint() {

// |flutter::DlOpReceiver|
void DlDispatcher::drawLine(const SkPoint& p0, const SkPoint& p1) {
auto path =
PathBuilder{}
.AddLine(skia_conversions::ToPoint(p0), skia_conversions::ToPoint(p1))
.SetConvexity(Convexity::kConvex)
.TakePath();
Paint paint = paint_;
paint.style = Paint::Style::kStroke;
canvas_.DrawPath(path, paint);
canvas_.DrawLine(skia_conversions::ToPoint(p0), skia_conversions::ToPoint(p1),
paint_);
}

// |flutter::DlOpReceiver|
Expand Down Expand Up @@ -879,17 +873,15 @@ void DlDispatcher::drawPoints(PointMode mode,
for (uint32_t i = 1; i < count; i += 2) {
Point p0 = skia_conversions::ToPoint(points[i - 1]);
Point p1 = skia_conversions::ToPoint(points[i]);
auto path = PathBuilder{}.AddLine(p0, p1).TakePath();
canvas_.DrawPath(path, paint);
canvas_.DrawLine(p0, p1, paint);
}
break;
case flutter::DlCanvas::PointMode::kPolygon:
if (count > 1) {
Point p0 = skia_conversions::ToPoint(points[0]);
for (uint32_t i = 1; i < count; i++) {
Point p1 = skia_conversions::ToPoint(points[i]);
auto path = PathBuilder{}.AddLine(p0, p1).TakePath();
canvas_.DrawPath(path, paint);
canvas_.DrawLine(p0, p1, paint);
p0 = p1;
}
}
Expand Down
2 changes: 2 additions & 0 deletions impeller/entity/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ impeller_component("entity") {
"geometry/fill_path_geometry.h",
"geometry/geometry.cc",
"geometry/geometry.h",
"geometry/line_geometry.cc",
"geometry/line_geometry.h",
"geometry/point_field_geometry.cc",
"geometry/point_field_geometry.h",
"geometry/rect_geometry.cc",
Expand Down
8 changes: 8 additions & 0 deletions impeller/entity/geometry/geometry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "impeller/entity/geometry/cover_geometry.h"
#include "impeller/entity/geometry/fill_path_geometry.h"
#include "impeller/entity/geometry/line_geometry.h"
#include "impeller/entity/geometry/point_field_geometry.h"
#include "impeller/entity/geometry/rect_geometry.h"
#include "impeller/entity/geometry/stroke_path_geometry.h"
Expand Down Expand Up @@ -113,6 +114,13 @@ std::unique_ptr<Geometry> Geometry::MakeRect(Rect rect) {
return std::make_unique<RectGeometry>(rect);
}

std::unique_ptr<Geometry> Geometry::MakeLine(Point p0,
Point p1,
Scalar width,
Cap cap) {
return std::make_unique<LineGeometry>(p0, p1, width, cap);
}

bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const {
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions impeller/entity/geometry/geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ class Geometry {

static std::unique_ptr<Geometry> MakeRect(Rect rect);

static std::unique_ptr<Geometry> MakeLine(Point p0,
Point p1,
Scalar width,
Cap cap);

static std::unique_ptr<Geometry> MakePointField(std::vector<Point> points,
Scalar radius,
bool round);
Expand Down
26 changes: 26 additions & 0 deletions impeller/entity/geometry/geometry_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,31 @@ TEST(EntityGeometryTest, FillPathGeometryCoversAreaNoInnerRect) {
ASSERT_FALSE(geometry->CoversArea({}, Rect()));
}

TEST(EntityGeometryTest, LineGeometryCoverage) {
{
auto geometry = Geometry::MakeLine({10, 10}, {20, 10}, 2, Cap::kButt);
EXPECT_EQ(geometry->GetCoverage({}), Rect::MakeLTRB(10, 9, 20, 11));
EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(10, 9, 20, 11)));
}

{
auto geometry = Geometry::MakeLine({10, 10}, {20, 10}, 2, Cap::kSquare);
EXPECT_EQ(geometry->GetCoverage({}), Rect::MakeLTRB(9, 9, 21, 11));
EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(9, 9, 21, 11)));
}

{
auto geometry = Geometry::MakeLine({10, 10}, {10, 20}, 2, Cap::kButt);
EXPECT_EQ(geometry->GetCoverage({}), Rect::MakeLTRB(9, 10, 11, 20));
EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(9, 10, 11, 20)));
}

{
auto geometry = Geometry::MakeLine({10, 10}, {10, 20}, 2, Cap::kSquare);
EXPECT_EQ(geometry->GetCoverage({}), Rect::MakeLTRB(9, 9, 11, 21));
EXPECT_TRUE(geometry->CoversArea({}, Rect::MakeLTRB(9, 9, 11, 21)));
}
}

} // namespace testing
} // namespace impeller
147 changes: 147 additions & 0 deletions impeller/entity/geometry/line_geometry.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "impeller/entity/geometry/line_geometry.h"

namespace impeller {

LineGeometry::LineGeometry(Point p0, Point p1, Scalar width, Cap cap)
: p0_(p0), p1_(p1), width_(width), cap_(cap) {
// Some of the code below is prepared to deal with things like coverage
// of a line with round caps, but more work is needed to deal with drawing
// the round end caps
FML_DCHECK(width >= 0);
FML_DCHECK(cap != Cap::kRound);
}

LineGeometry::~LineGeometry() = default;

bool LineGeometry::ComputeCorners(Point corners[4],
const Matrix& transform,
bool extend_endpoints) const {
auto determinant = transform.GetDeterminant();
if (determinant == 0) {
return false;
}

Scalar min_size = 1.0f / sqrt(std::abs(determinant));
Scalar stroke_half_width = std::max(width_, min_size) * 0.5f;

Point along = p1_ - p0_;
Scalar length = along.GetLength();
if (length < kEhCloseEnough) {
if (!extend_endpoints) {
// We won't enclose any pixels unless the endpoints are extended
return false;
}
along = {stroke_half_width, 0};
} else {
along *= stroke_half_width / length;
}
Point across = {along.y, -along.x};
corners[0] = p0_ - across;
corners[1] = p1_ - across;
corners[2] = p0_ + across;
corners[3] = p1_ + across;
if (extend_endpoints) {
corners[0] -= along;
corners[1] += along;
corners[2] -= along;
corners[3] += along;
}
return true;
}

GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) {
auto& host_buffer = pass.GetTransientsBuffer();

Point corners[4];
if (!ComputeCorners(corners, entity.GetTransformation(),
cap_ == Cap::kSquare)) {
return {};
}

return GeometryResult{
.type = PrimitiveType::kTriangleStrip,
.vertex_buffer =
{
.vertex_buffer = host_buffer.Emplace(corners, 8 * sizeof(float),
alignof(float)),
.vertex_count = 4,
.index_type = IndexType::kNone,
},
.transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) *
entity.GetTransformation(),
.prevent_overdraw = false,
};
}

// |Geometry|
GeometryResult LineGeometry::GetPositionUVBuffer(Rect texture_coverage,
Matrix effect_transform,
const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) {
auto& host_buffer = pass.GetTransientsBuffer();

auto uv_transform =
texture_coverage.GetNormalizingTransform() * effect_transform;
Point corners[4];
if (!ComputeCorners(corners, entity.GetTransformation(),
cap_ == Cap::kSquare)) {
return {};
}

std::vector<Point> data(8);
for (auto i = 0u, j = 0u; i < 8; i += 2, j++) {
data[i] = corners[j];
data[i + 1] = uv_transform * corners[j];
}

return GeometryResult{
.type = PrimitiveType::kTriangleStrip,
.vertex_buffer =
{
.vertex_buffer = host_buffer.Emplace(
data.data(), 16 * sizeof(float), alignof(float)),
.vertex_count = 4,
.index_type = IndexType::kNone,
},
.transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) *
entity.GetTransformation(),
.prevent_overdraw = false,
};
}

GeometryVertexType LineGeometry::GetVertexType() const {
return GeometryVertexType::kPosition;
}

std::optional<Rect> LineGeometry::GetCoverage(const Matrix& transform) const {
Point corners[4];
if (!ComputeCorners(corners, transform, cap_ != Cap::kButt)) {
return {};
}

for (int i = 0; i < 4; i++) {
corners[i] = transform * corners[i];
}
return Rect::MakePointBounds(std::begin(corners), std::end(corners));
}

bool LineGeometry::CoversArea(const Matrix& transform, const Rect& rect) const {
if (!transform.IsTranslationScaleOnly() || !IsAxisAlignedRect()) {
return false;
}
auto coverage = GetCoverage(transform);
return coverage.has_value() ? coverage->Contains(rect) : false;
}

bool LineGeometry::IsAxisAlignedRect() const {
return p0_.x == p1_.x || p0_.y == p1_.y;
}

} // namespace impeller
Loading

0 comments on commit 77a12e4

Please sign in to comment.