Flutter Impeller
entity_unittests.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <algorithm>
6 #include <cstring>
7 #include <memory>
8 #include <optional>
9 #include <utility>
10 #include <vector>
11 
12 #include "flutter/display_list/geometry/dl_path_builder.h"
13 #include "flutter/display_list/testing/dl_test_snippets.h"
14 #include "fml/logging.h"
15 #include "gtest/gtest.h"
17 #include "impeller/core/formats.h"
19 #include "impeller/core/raw_ptr.h"
39 #include "impeller/entity/entity.h"
58 #include "impeller/renderer/testing/mocks.h"
60 #include "third_party/abseil-cpp/absl/status/status_matchers.h"
61 #include "third_party/imgui/imgui.h"
62 
63 // TODO(zanderso): https://github.com/flutter/flutter/issues/127701
64 // NOLINTBEGIN(bugprone-unchecked-optional-access)
65 
66 namespace impeller {
67 namespace testing {
68 
69 using EntityTest = EntityPlayground;
71 
72 TEST_P(EntityTest, CanCreateEntity) {
73  Entity entity;
74  ASSERT_TRUE(entity.GetTransform().IsIdentity());
75 }
76 
77 TEST_P(EntityTest, FilterCoverageRespectsCropRect) {
78  auto image = CreateTextureForFixture("boston.jpg");
80  FilterInput::Make({image}));
81 
82  // Without the crop rect (default behavior).
83  {
84  auto actual = filter->GetCoverage({});
85  auto expected = Rect::MakeSize(image->GetSize());
86 
87  ASSERT_TRUE(actual.has_value());
88  ASSERT_RECT_NEAR(actual.value(), expected);
89  }
90 
91  // With the crop rect.
92  {
93  auto expected = Rect::MakeLTRB(50, 50, 100, 100);
94  filter->SetCoverageHint(expected);
95  auto actual = filter->GetCoverage({});
96 
97  ASSERT_TRUE(actual.has_value());
98  ASSERT_RECT_NEAR(actual.value(), expected);
99  }
100 }
101 
102 TEST_P(EntityTest, GeometryBoundsAreTransformed) {
103  auto geometry = Geometry::MakeRect(Rect::MakeXYWH(100, 100, 100, 100));
104  auto transform = Matrix::MakeScale({2.0, 2.0, 2.0});
105 
106  ASSERT_RECT_NEAR(geometry->GetCoverage(transform).value(),
107  Rect::MakeXYWH(200, 200, 200, 200));
108 }
109 
110 TEST_P(EntityTest, ThreeStrokesInOnePath) {
111  flutter::DlPath path = flutter::DlPathBuilder{}
112  .MoveTo({100, 100})
113  .LineTo({100, 200})
114  .MoveTo({100, 300})
115  .LineTo({100, 400})
116  .MoveTo({100, 500})
117  .LineTo({100, 600})
118  .TakePath();
119 
120  Entity entity;
121  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
122  std::unique_ptr<Geometry> geom =
123  Geometry::MakeStrokePath(path, {.width = 5.0f});
124  auto contents = std::make_unique<SolidColorContents>(geom.get());
125  contents->SetColor(Color::Red());
126  entity.SetContents(std::move(contents));
127  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
128 }
129 
130 TEST_P(EntityTest, StrokeWithTextureContents) {
131  auto bridge = CreateTextureForFixture("bay_bridge.jpg");
132  flutter::DlPath path = flutter::DlPathBuilder{}
133  .MoveTo({100, 100})
134  .LineTo({100, 200})
135  .MoveTo({100, 300})
136  .LineTo({100, 400})
137  .MoveTo({100, 500})
138  .LineTo({100, 600})
139  .TakePath();
140 
141  Entity entity;
142  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
143  std::unique_ptr<Geometry> geom =
144  Geometry::MakeStrokePath(path, {.width = 100.0f});
145  auto contents = std::make_unique<TiledTextureContents>(geom.get());
146  contents->SetTexture(bridge);
147  contents->SetTileModes(Entity::TileMode::kClamp, Entity::TileMode::kClamp);
148  entity.SetContents(std::move(contents));
149  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
150 }
151 
152 TEST_P(EntityTest, TriangleInsideASquare) {
153  auto callback = [&](ContentContext& context, RenderPass& pass) {
154  Point offset(100, 100);
155 
156  static PlaygroundPoint point_a(Point(10, 10) + offset, 20, Color::White());
157  Point a = DrawPlaygroundPoint(point_a);
158  static PlaygroundPoint point_b(Point(210, 10) + offset, 20, Color::White());
159  Point b = DrawPlaygroundPoint(point_b);
160  static PlaygroundPoint point_c(Point(210, 210) + offset, 20,
161  Color::White());
162  Point c = DrawPlaygroundPoint(point_c);
163  static PlaygroundPoint point_d(Point(10, 210) + offset, 20, Color::White());
164  Point d = DrawPlaygroundPoint(point_d);
165  static PlaygroundPoint point_e(Point(50, 50) + offset, 20, Color::White());
166  Point e = DrawPlaygroundPoint(point_e);
167  static PlaygroundPoint point_f(Point(100, 50) + offset, 20, Color::White());
168  Point f = DrawPlaygroundPoint(point_f);
169  static PlaygroundPoint point_g(Point(50, 150) + offset, 20, Color::White());
170  Point g = DrawPlaygroundPoint(point_g);
171  flutter::DlPath path = flutter::DlPathBuilder{}
172  .MoveTo(a)
173  .LineTo(b)
174  .LineTo(c)
175  .LineTo(d)
176  .Close()
177  .MoveTo(e)
178  .LineTo(f)
179  .LineTo(g)
180  .Close()
181  .TakePath();
182 
183  Entity entity;
184  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
185  std::unique_ptr<Geometry> geom =
186  Geometry::MakeStrokePath(path, {.width = 20.0});
187  auto contents = std::make_unique<SolidColorContents>(geom.get());
188  contents->SetColor(Color::Red());
189  entity.SetContents(std::move(contents));
190 
191  return entity.Render(context, pass);
192  };
193  ASSERT_TRUE(OpenPlaygroundHere(callback));
194 }
195 
196 TEST_P(EntityTest, StrokeCapAndJoinTest) {
197  const Point padding(300, 250);
198  const Point margin(140, 180);
199 
200  auto callback = [&](ContentContext& context, RenderPass& pass) {
201  // Slightly above sqrt(2) by default, so that right angles are just below
202  // the limit and acute angles are over the limit (causing them to get
203  // beveled).
204  static Scalar miter_limit = 1.41421357;
205  static Scalar width = 30;
206 
207  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
208  {
209  ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30);
210  ImGui::SliderFloat("Stroke width", &width, 0, 100);
211  if (ImGui::Button("Reset")) {
212  miter_limit = 1.41421357;
213  width = 30;
214  }
215  }
216  ImGui::End();
217 
218  auto world_matrix = Matrix::MakeScale(GetContentScale());
219  auto render_path = [width = width, &context, &pass, &world_matrix](
220  const flutter::DlPath& path, Cap cap, Join join) {
221  std::unique_ptr<Geometry> geom =
223  .width = width,
224  .cap = cap,
225  .join = join,
226  .miter_limit = miter_limit,
227  });
228  auto contents = std::make_unique<SolidColorContents>(geom.get());
229  contents->SetColor(Color::Red());
230 
231  Entity entity;
232  entity.SetTransform(world_matrix);
233  entity.SetContents(std::move(contents));
234 
235  auto coverage = entity.GetCoverage();
236  if (coverage.has_value()) {
237  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(
238  flutter::DlPath::MakeRect(entity.GetCoverage().value()));
239 
240  auto bounds_contents = std::make_unique<SolidColorContents>(geom.get());
241  bounds_contents->SetColor(Color::Green().WithAlpha(0.5));
242  Entity bounds_entity;
243  bounds_entity.SetContents(std::move(bounds_contents));
244  bounds_entity.Render(context, pass);
245  }
246 
247  entity.Render(context, pass);
248  };
249 
250  const Point a_def(0, 0), b_def(0, 100), c_def(150, 0), d_def(150, -100),
251  e_def(75, 75);
252  const Scalar r = 30;
253  // Cap::kButt demo.
254  {
255  Point off = Point(0, 0) * padding + margin;
256  static PlaygroundPoint point_a(off + a_def, r, Color::Black());
257  static PlaygroundPoint point_b(off + b_def, r, Color::White());
258  auto [a, b] = DrawPlaygroundLine(point_a, point_b);
259  static PlaygroundPoint point_c(off + c_def, r, Color::Black());
260  static PlaygroundPoint point_d(off + d_def, r, Color::White());
261  auto [c, d] = DrawPlaygroundLine(point_c, point_d);
262  render_path(flutter::DlPathBuilder{} //
263  .MoveTo(a)
264  .CubicCurveTo(b, d, c)
265  .TakePath(),
267  }
268 
269  // Cap::kSquare demo.
270  {
271  Point off = Point(1, 0) * padding + margin;
272  static PlaygroundPoint point_a(off + a_def, r, Color::Black());
273  static PlaygroundPoint point_b(off + b_def, r, Color::White());
274  auto [a, b] = DrawPlaygroundLine(point_a, point_b);
275  static PlaygroundPoint point_c(off + c_def, r, Color::Black());
276  static PlaygroundPoint point_d(off + d_def, r, Color::White());
277  auto [c, d] = DrawPlaygroundLine(point_c, point_d);
278  render_path(flutter::DlPathBuilder{} //
279  .MoveTo(a)
280  .CubicCurveTo(b, d, c)
281  .TakePath(),
283  }
284 
285  // Cap::kRound demo.
286  {
287  Point off = Point(2, 0) * padding + margin;
288  static PlaygroundPoint point_a(off + a_def, r, Color::Black());
289  static PlaygroundPoint point_b(off + b_def, r, Color::White());
290  auto [a, b] = DrawPlaygroundLine(point_a, point_b);
291  static PlaygroundPoint point_c(off + c_def, r, Color::Black());
292  static PlaygroundPoint point_d(off + d_def, r, Color::White());
293  auto [c, d] = DrawPlaygroundLine(point_c, point_d);
294  render_path(flutter::DlPathBuilder{} //
295  .MoveTo(a)
296  .CubicCurveTo(b, d, c)
297  .TakePath(),
299  }
300 
301  // Join::kBevel demo.
302  {
303  Point off = Point(0, 1) * padding + margin;
304  static PlaygroundPoint point_a =
305  PlaygroundPoint(off + a_def, r, Color::White());
306  static PlaygroundPoint point_b =
307  PlaygroundPoint(off + e_def, r, Color::White());
308  static PlaygroundPoint point_c =
309  PlaygroundPoint(off + c_def, r, Color::White());
310  Point a = DrawPlaygroundPoint(point_a);
311  Point b = DrawPlaygroundPoint(point_b);
312  Point c = DrawPlaygroundPoint(point_c);
313  render_path(flutter::DlPathBuilder{} //
314  .MoveTo(a)
315  .LineTo(b)
316  .LineTo(c)
317  .Close()
318  .TakePath(),
320  }
321 
322  // Join::kMiter demo.
323  {
324  Point off = Point(1, 1) * padding + margin;
325  static PlaygroundPoint point_a(off + a_def, r, Color::White());
326  static PlaygroundPoint point_b(off + e_def, r, Color::White());
327  static PlaygroundPoint point_c(off + c_def, r, Color::White());
328  Point a = DrawPlaygroundPoint(point_a);
329  Point b = DrawPlaygroundPoint(point_b);
330  Point c = DrawPlaygroundPoint(point_c);
331  render_path(flutter::DlPathBuilder{} //
332  .MoveTo(a)
333  .LineTo(b)
334  .LineTo(c)
335  .Close()
336  .TakePath(),
338  }
339 
340  // Join::kRound demo.
341  {
342  Point off = Point(2, 1) * padding + margin;
343  static PlaygroundPoint point_a(off + a_def, r, Color::White());
344  static PlaygroundPoint point_b(off + e_def, r, Color::White());
345  static PlaygroundPoint point_c(off + c_def, r, Color::White());
346  Point a = DrawPlaygroundPoint(point_a);
347  Point b = DrawPlaygroundPoint(point_b);
348  Point c = DrawPlaygroundPoint(point_c);
349  render_path(flutter::DlPathBuilder{} //
350  .MoveTo(a)
351  .LineTo(b)
352  .LineTo(c)
353  .Close()
354  .TakePath(),
356  }
357 
358  return true;
359  };
360  ASSERT_TRUE(OpenPlaygroundHere(callback));
361 }
362 
363 TEST_P(EntityTest, CubicCurveTest) {
364  // Compare with https://fiddle.skia.org/c/b3625f26122c9de7afe7794fcf25ead3
365  flutter::DlPath path =
366  flutter::DlPathBuilder{}
367  .MoveTo({237.164, 125.003})
368  .CubicCurveTo({236.709, 125.184}, {236.262, 125.358},
369  {235.81, 125.538})
370  .CubicCurveTo({235.413, 125.68}, {234.994, 125.832},
371  {234.592, 125.977})
372  .CubicCurveTo({234.592, 125.977}, {234.591, 125.977},
373  {234.59, 125.977})
374  .CubicCurveTo({222.206, 130.435}, {207.708, 135.753},
375  {192.381, 141.429})
376  .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160})
377  .Close()
378  .TakePath();
379  Entity entity;
380  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
381 
382  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
383 
384  auto contents = std::make_shared<SolidColorContents>(geom.get());
385  contents->SetColor(Color::Red());
386 
387  entity.SetContents(contents);
388  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
389 }
390 
391 TEST_P(EntityTest, CanDrawCorrectlyWithRotatedTransform) {
392  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
393  const char* input_axis[] = {"X", "Y", "Z"};
394  static int rotation_axis_index = 0;
395  static float rotation = 0;
396  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
397  ImGui::SliderFloat("Rotation", &rotation, -kPi, kPi);
398  ImGui::Combo("Rotation Axis", &rotation_axis_index, input_axis,
399  sizeof(input_axis) / sizeof(char*));
400  Matrix rotation_matrix;
401  switch (rotation_axis_index) {
402  case 0:
403  rotation_matrix = Matrix::MakeRotationX(Radians(rotation));
404  break;
405  case 1:
406  rotation_matrix = Matrix::MakeRotationY(Radians(rotation));
407  break;
408  case 2:
409  rotation_matrix = Matrix::MakeRotationZ(Radians(rotation));
410  break;
411  default:
412  rotation_matrix = Matrix{};
413  break;
414  }
415 
416  if (ImGui::Button("Reset")) {
417  rotation = 0;
418  }
419  ImGui::End();
420  Matrix current_transform =
421  Matrix::MakeScale(GetContentScale())
423  Vector3(Point(pass.GetRenderTargetSize().width / 2.0,
424  pass.GetRenderTargetSize().height / 2.0)));
425  Matrix result_transform = current_transform * rotation_matrix;
426  flutter::DlPath path =
427  flutter::DlPath::MakeRect(Rect::MakeXYWH(-300, -400, 600, 800));
428 
429  Entity entity;
430  entity.SetTransform(result_transform);
431 
432  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
433 
434  auto contents = std::make_shared<SolidColorContents>(geom.get());
435  contents->SetColor(Color::Red());
436 
437  entity.SetContents(contents);
438  return entity.Render(context, pass);
439  };
440  ASSERT_TRUE(OpenPlaygroundHere(callback));
441 }
442 
443 TEST_P(EntityTest, CubicCurveAndOverlapTest) {
444  // Compare with https://fiddle.skia.org/c/7a05a3e186c65a8dfb732f68020aae06
445  flutter::DlPath path =
446  flutter::DlPathBuilder{}
447  .MoveTo({359.934, 96.6335})
448  .CubicCurveTo({358.189, 96.7055}, {356.436, 96.7908},
449  {354.673, 96.8895})
450  .CubicCurveTo({354.571, 96.8953}, {354.469, 96.9016},
451  {354.367, 96.9075})
452  .CubicCurveTo({352.672, 97.0038}, {350.969, 97.113},
453  {349.259, 97.2355})
454  .CubicCurveTo({349.048, 97.2506}, {348.836, 97.2678},
455  {348.625, 97.2834})
456  .CubicCurveTo({347.019, 97.4014}, {345.407, 97.5299},
457  {343.789, 97.6722})
458  .CubicCurveTo({343.428, 97.704}, {343.065, 97.7402},
459  {342.703, 97.7734})
460  .CubicCurveTo({341.221, 97.9086}, {339.736, 98.0505},
461  {338.246, 98.207})
462  .CubicCurveTo({337.702, 98.2642}, {337.156, 98.3292},
463  {336.612, 98.3894})
464  .CubicCurveTo({335.284, 98.5356}, {333.956, 98.6837},
465  {332.623, 98.8476})
466  .CubicCurveTo({332.495, 98.8635}, {332.366, 98.8818},
467  {332.237, 98.8982})
468  .LineTo({332.237, 102.601})
469  .LineTo({321.778, 102.601})
470  .LineTo({321.778, 100.382})
471  .CubicCurveTo({321.572, 100.413}, {321.367, 100.442},
472  {321.161, 100.476})
473  .CubicCurveTo({319.22, 100.79}, {317.277, 101.123},
474  {315.332, 101.479})
475  .CubicCurveTo({315.322, 101.481}, {315.311, 101.482},
476  {315.301, 101.484})
477  .LineTo({310.017, 105.94})
478  .LineTo({309.779, 105.427})
479  .LineTo({314.403, 101.651})
480  .CubicCurveTo({314.391, 101.653}, {314.379, 101.656},
481  {314.368, 101.658})
482  .CubicCurveTo({312.528, 102.001}, {310.687, 102.366},
483  {308.846, 102.748})
484  .CubicCurveTo({307.85, 102.955}, {306.855, 103.182}, {305.859, 103.4})
485  .CubicCurveTo({305.048, 103.579}, {304.236, 103.75},
486  {303.425, 103.936})
487  .LineTo({299.105, 107.578})
488  .LineTo({298.867, 107.065})
489  .LineTo({302.394, 104.185})
490  .LineTo({302.412, 104.171})
491  .CubicCurveTo({301.388, 104.409}, {300.366, 104.67},
492  {299.344, 104.921})
493  .CubicCurveTo({298.618, 105.1}, {297.89, 105.269}, {297.165, 105.455})
494  .CubicCurveTo({295.262, 105.94}, {293.36, 106.445},
495  {291.462, 106.979})
496  .CubicCurveTo({291.132, 107.072}, {290.802, 107.163},
497  {290.471, 107.257})
498  .CubicCurveTo({289.463, 107.544}, {288.455, 107.839},
499  {287.449, 108.139})
500  .CubicCurveTo({286.476, 108.431}, {285.506, 108.73},
501  {284.536, 109.035})
502  .CubicCurveTo({283.674, 109.304}, {282.812, 109.579},
503  {281.952, 109.859})
504  .CubicCurveTo({281.177, 110.112}, {280.406, 110.377},
505  {279.633, 110.638})
506  .CubicCurveTo({278.458, 111.037}, {277.256, 111.449},
507  {276.803, 111.607})
508  .CubicCurveTo({276.76, 111.622}, {276.716, 111.637},
509  {276.672, 111.653})
510  .CubicCurveTo({275.017, 112.239}, {273.365, 112.836},
511  {271.721, 113.463})
512  .LineTo({271.717, 113.449})
513  .CubicCurveTo({271.496, 113.496}, {271.238, 113.559},
514  {270.963, 113.628})
515  .CubicCurveTo({270.893, 113.645}, {270.822, 113.663},
516  {270.748, 113.682})
517  .CubicCurveTo({270.468, 113.755}, {270.169, 113.834},
518  {269.839, 113.926})
519  .CubicCurveTo({269.789, 113.94}, {269.732, 113.957},
520  {269.681, 113.972})
521  .CubicCurveTo({269.391, 114.053}, {269.081, 114.143},
522  {268.756, 114.239})
523  .CubicCurveTo({268.628, 114.276}, {268.5, 114.314},
524  {268.367, 114.354})
525  .CubicCurveTo({268.172, 114.412}, {267.959, 114.478},
526  {267.752, 114.54})
527  .CubicCurveTo({263.349, 115.964}, {258.058, 117.695},
528  {253.564, 119.252})
529  .CubicCurveTo({253.556, 119.255}, {253.547, 119.258},
530  {253.538, 119.261})
531  .CubicCurveTo({251.844, 119.849}, {250.056, 120.474},
532  {248.189, 121.131})
533  .CubicCurveTo({248, 121.197}, {247.812, 121.264}, {247.621, 121.331})
534  .CubicCurveTo({247.079, 121.522}, {246.531, 121.715},
535  {245.975, 121.912})
536  .CubicCurveTo({245.554, 122.06}, {245.126, 122.212},
537  {244.698, 122.364})
538  .CubicCurveTo({244.071, 122.586}, {243.437, 122.811},
539  {242.794, 123.04})
540  .CubicCurveTo({242.189, 123.255}, {241.58, 123.472},
541  {240.961, 123.693})
542  .CubicCurveTo({240.659, 123.801}, {240.357, 123.909},
543  {240.052, 124.018})
544  .CubicCurveTo({239.12, 124.351}, {238.18, 124.687}, {237.22, 125.032})
545  .LineTo({237.164, 125.003})
546  .CubicCurveTo({236.709, 125.184}, {236.262, 125.358},
547  {235.81, 125.538})
548  .CubicCurveTo({235.413, 125.68}, {234.994, 125.832},
549  {234.592, 125.977})
550  .CubicCurveTo({234.592, 125.977}, {234.591, 125.977},
551  {234.59, 125.977})
552  .CubicCurveTo({222.206, 130.435}, {207.708, 135.753},
553  {192.381, 141.429})
554  .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160})
555  .LineTo({360, 160})
556  .LineTo({360, 119.256})
557  .LineTo({360, 106.332})
558  .LineTo({360, 96.6307})
559  .CubicCurveTo({359.978, 96.6317}, {359.956, 96.6326},
560  {359.934, 96.6335})
561  .Close()
562  .MoveTo({337.336, 124.143})
563  .CubicCurveTo({337.274, 122.359}, {338.903, 121.511},
564  {338.903, 121.511})
565  .CubicCurveTo({338.903, 121.511}, {338.96, 123.303},
566  {337.336, 124.143})
567  .Close()
568  .MoveTo({340.082, 121.849})
569  .CubicCurveTo({340.074, 121.917}, {340.062, 121.992},
570  {340.046, 122.075})
571  .CubicCurveTo({340.039, 122.109}, {340.031, 122.142},
572  {340.023, 122.177})
573  .CubicCurveTo({340.005, 122.26}, {339.98, 122.346},
574  {339.952, 122.437})
575  .CubicCurveTo({339.941, 122.473}, {339.931, 122.507},
576  {339.918, 122.544})
577  .CubicCurveTo({339.873, 122.672}, {339.819, 122.804},
578  {339.75, 122.938})
579  .CubicCurveTo({339.747, 122.944}, {339.743, 122.949},
580  {339.74, 122.955})
581  .CubicCurveTo({339.674, 123.08}, {339.593, 123.205},
582  {339.501, 123.328})
583  .CubicCurveTo({339.473, 123.366}, {339.441, 123.401},
584  {339.41, 123.438})
585  .CubicCurveTo({339.332, 123.534}, {339.243, 123.625},
586  {339.145, 123.714})
587  .CubicCurveTo({339.105, 123.75}, {339.068, 123.786},
588  {339.025, 123.821})
589  .CubicCurveTo({338.881, 123.937}, {338.724, 124.048},
590  {338.539, 124.143})
591  .CubicCurveTo({338.532, 123.959}, {338.554, 123.79},
592  {338.58, 123.626})
593  .CubicCurveTo({338.58, 123.625}, {338.58, 123.625}, {338.58, 123.625})
594  .CubicCurveTo({338.607, 123.455}, {338.65, 123.299},
595  {338.704, 123.151})
596  .CubicCurveTo({338.708, 123.14}, {338.71, 123.127},
597  {338.714, 123.117})
598  .CubicCurveTo({338.769, 122.971}, {338.833, 122.838},
599  {338.905, 122.712})
600  .CubicCurveTo({338.911, 122.702}, {338.916, 122.69200000000001},
601  {338.922, 122.682})
602  .CubicCurveTo({338.996, 122.557}, {339.072, 122.444},
603  {339.155, 122.34})
604  .CubicCurveTo({339.161, 122.333}, {339.166, 122.326},
605  {339.172, 122.319})
606  .CubicCurveTo({339.256, 122.215}, {339.339, 122.12},
607  {339.425, 122.037})
608  .CubicCurveTo({339.428, 122.033}, {339.431, 122.03},
609  {339.435, 122.027})
610  .CubicCurveTo({339.785, 121.687}, {340.106, 121.511},
611  {340.106, 121.511})
612  .CubicCurveTo({340.106, 121.511}, {340.107, 121.645},
613  {340.082, 121.849})
614  .Close()
615  .MoveTo({340.678, 113.245})
616  .CubicCurveTo({340.594, 113.488}, {340.356, 113.655},
617  {340.135, 113.775})
618  .CubicCurveTo({339.817, 113.948}, {339.465, 114.059},
619  {339.115, 114.151})
620  .CubicCurveTo({338.251, 114.379}, {337.34, 114.516},
621  {336.448, 114.516})
622  .CubicCurveTo({335.761, 114.516}, {335.072, 114.527},
623  {334.384, 114.513})
624  .CubicCurveTo({334.125, 114.508}, {333.862, 114.462},
625  {333.605, 114.424})
626  .CubicCurveTo({332.865, 114.318}, {332.096, 114.184},
627  {331.41, 113.883})
628  .CubicCurveTo({330.979, 113.695}, {330.442, 113.34},
629  {330.672, 112.813})
630  .CubicCurveTo({331.135, 111.755}, {333.219, 112.946},
631  {334.526, 113.833})
632  .CubicCurveTo({334.54, 113.816}, {334.554, 113.8}, {334.569, 113.784})
633  .CubicCurveTo({333.38, 112.708}, {331.749, 110.985},
634  {332.76, 110.402})
635  .CubicCurveTo({333.769, 109.82}, {334.713, 111.93},
636  {335.228, 113.395})
637  .CubicCurveTo({334.915, 111.889}, {334.59, 109.636},
638  {335.661, 109.592})
639  .CubicCurveTo({336.733, 109.636}, {336.408, 111.889},
640  {336.07, 113.389})
641  .CubicCurveTo({336.609, 111.93}, {337.553, 109.82},
642  {338.563, 110.402})
643  .CubicCurveTo({339.574, 110.984}, {337.942, 112.708},
644  {336.753, 113.784})
645  .CubicCurveTo({336.768, 113.8}, {336.782, 113.816},
646  {336.796, 113.833})
647  .CubicCurveTo({338.104, 112.946}, {340.187, 111.755},
648  {340.65, 112.813})
649  .CubicCurveTo({340.71, 112.95}, {340.728, 113.102},
650  {340.678, 113.245})
651  .Close()
652  .MoveTo({346.357, 106.771})
653  .CubicCurveTo({346.295, 104.987}, {347.924, 104.139},
654  {347.924, 104.139})
655  .CubicCurveTo({347.924, 104.139}, {347.982, 105.931},
656  {346.357, 106.771})
657  .Close()
658  .MoveTo({347.56, 106.771})
659  .CubicCurveTo({347.498, 104.987}, {349.127, 104.139},
660  {349.127, 104.139})
661  .CubicCurveTo({349.127, 104.139}, {349.185, 105.931},
662  {347.56, 106.771})
663  .Close()
664  .TakePath();
665  Entity entity;
666  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
667 
668  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
669 
670  auto contents = std::make_shared<SolidColorContents>(geom.get());
671  contents->SetColor(Color::Red());
672 
673  entity.SetContents(contents);
674  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
675 }
676 
677 TEST_P(EntityTest, SolidColorContentsStrokeSetStrokeCapsAndJoins) {
678  {
679  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{});
680  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
681  // Defaults.
682  ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kButt);
683  ASSERT_EQ(path_geometry->GetStrokeJoin(), Join::kMiter);
684  }
685 
686  {
687  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{}, //
688  {
689  .width = 1.0f,
690  .cap = Cap::kSquare,
691  .miter_limit = 4.0f,
692  });
693  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
694  ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kSquare);
695  }
696 
697  {
698  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{}, //
699  {
700  .width = 1.0f,
701  .cap = Cap::kRound,
702  .miter_limit = 4.0f,
703  });
704  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
705  ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kRound);
706  }
707 }
708 
709 TEST_P(EntityTest, SolidColorContentsStrokeSetMiterLimit) {
710  {
711  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{});
712  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
713  ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 4);
714  }
715 
716  {
717  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{}, //
718  {
719  .width = 1.0f,
720  .miter_limit = 8.0f,
721  });
722  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
723  ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 8);
724  }
725 
726  {
727  auto geometry = Geometry::MakeStrokePath(flutter::DlPath{}, //
728  {
729  .width = 1.0f,
730  .miter_limit = -1.0f,
731  });
732  auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get());
733  ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 4);
734  }
735 }
736 
737 TEST_P(EntityTest, BlendingModeOptions) {
738  std::vector<const char*> blend_mode_names;
739  std::vector<BlendMode> blend_mode_values;
740  {
741  // Force an exhausiveness check with a switch. When adding blend modes,
742  // update this switch with a new name/value to make it selectable in the
743  // test GUI.
744 
745  const BlendMode b{};
746  static_assert(b == BlendMode::kClear); // Ensure the first item in
747  // the switch is the first
748  // item in the enum.
750  switch (b) {
751  case BlendMode::kClear:
752  blend_mode_names.push_back("Clear");
753  blend_mode_values.push_back(BlendMode::kClear);
754  case BlendMode::kSrc:
755  blend_mode_names.push_back("Source");
756  blend_mode_values.push_back(BlendMode::kSrc);
757  case BlendMode::kDst:
758  blend_mode_names.push_back("Destination");
759  blend_mode_values.push_back(BlendMode::kDst);
760  case BlendMode::kSrcOver:
761  blend_mode_names.push_back("SourceOver");
762  blend_mode_values.push_back(BlendMode::kSrcOver);
763  case BlendMode::kDstOver:
764  blend_mode_names.push_back("DestinationOver");
765  blend_mode_values.push_back(BlendMode::kDstOver);
766  case BlendMode::kSrcIn:
767  blend_mode_names.push_back("SourceIn");
768  blend_mode_values.push_back(BlendMode::kSrcIn);
769  case BlendMode::kDstIn:
770  blend_mode_names.push_back("DestinationIn");
771  blend_mode_values.push_back(BlendMode::kDstIn);
772  case BlendMode::kSrcOut:
773  blend_mode_names.push_back("SourceOut");
774  blend_mode_values.push_back(BlendMode::kSrcOut);
775  case BlendMode::kDstOut:
776  blend_mode_names.push_back("DestinationOut");
777  blend_mode_values.push_back(BlendMode::kDstOut);
778  case BlendMode::kSrcATop:
779  blend_mode_names.push_back("SourceATop");
780  blend_mode_values.push_back(BlendMode::kSrcATop);
781  case BlendMode::kDstATop:
782  blend_mode_names.push_back("DestinationATop");
783  blend_mode_values.push_back(BlendMode::kDstATop);
784  case BlendMode::kXor:
785  blend_mode_names.push_back("Xor");
786  blend_mode_values.push_back(BlendMode::kXor);
787  case BlendMode::kPlus:
788  blend_mode_names.push_back("Plus");
789  blend_mode_values.push_back(BlendMode::kPlus);
791  blend_mode_names.push_back("Modulate");
792  blend_mode_values.push_back(BlendMode::kModulate);
793  };
794  }
795 
796  auto callback = [&](ContentContext& context, RenderPass& pass) {
797  auto world_matrix = Matrix::MakeScale(GetContentScale());
798  auto draw_rect = [&context, &pass, &world_matrix](
799  Rect rect, Color color, BlendMode blend_mode) -> bool {
802 
804  {
805  auto r = rect.GetLTRB();
806  vtx_builder.AddVertices({
807  {Point(r[0], r[1])},
808  {Point(r[2], r[1])},
809  {Point(r[2], r[3])},
810  {Point(r[0], r[1])},
811  {Point(r[2], r[3])},
812  {Point(r[0], r[3])},
813  });
814  }
815 
816  pass.SetCommandLabel("Blended Rectangle");
817  auto options = OptionsFromPass(pass);
818  options.blend_mode = blend_mode;
819  options.primitive_type = PrimitiveType::kTriangle;
820  pass.SetPipeline(context.GetSolidFillPipeline(options));
821  pass.SetVertexBuffer(
822  vtx_builder.CreateVertexBuffer(context.GetTransientsDataBuffer(),
823  context.GetTransientsIndexesBuffer()));
824 
825  VS::FrameInfo frame_info;
826  frame_info.mvp = pass.GetOrthographicTransform() * world_matrix;
827  VS::BindFrameInfo(
828  pass, context.GetTransientsDataBuffer().EmplaceUniform(frame_info));
829  FS::FragInfo frag_info;
830  frag_info.color = color.Premultiply();
831  FS::BindFragInfo(
832  pass, context.GetTransientsDataBuffer().EmplaceUniform(frag_info));
833  return pass.Draw().ok();
834  };
835 
836  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
837  static Color color1(1, 0, 0, 0.5), color2(0, 1, 0, 0.5);
838  ImGui::ColorEdit4("Color 1", reinterpret_cast<float*>(&color1));
839  ImGui::ColorEdit4("Color 2", reinterpret_cast<float*>(&color2));
840  static int current_blend_index = 3;
841  ImGui::ListBox("Blending mode", &current_blend_index,
842  blend_mode_names.data(), blend_mode_names.size());
843  ImGui::End();
844 
845  BlendMode selected_mode = blend_mode_values[current_blend_index];
846 
847  Point a, b, c, d;
848  static PlaygroundPoint point_a(Point(400, 100), 20, Color::White());
849  static PlaygroundPoint point_b(Point(200, 300), 20, Color::White());
850  std::tie(a, b) = DrawPlaygroundLine(point_a, point_b);
851  static PlaygroundPoint point_c(Point(470, 190), 20, Color::White());
852  static PlaygroundPoint point_d(Point(270, 390), 20, Color::White());
853  std::tie(c, d) = DrawPlaygroundLine(point_c, point_d);
854 
855  bool result = true;
856  result = result &&
857  draw_rect(Rect::MakeXYWH(0, 0, pass.GetRenderTargetSize().width,
858  pass.GetRenderTargetSize().height),
860  result = result && draw_rect(Rect::MakeLTRB(a.x, a.y, b.x, b.y), color1,
862  result = result && draw_rect(Rect::MakeLTRB(c.x, c.y, d.x, d.y), color2,
863  selected_mode);
864  return result;
865  };
866  ASSERT_TRUE(OpenPlaygroundHere(callback));
867 }
868 
869 TEST_P(EntityTest, BezierCircleScaled) {
870  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
871  static float scale = 20;
872 
873  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
874  ImGui::SliderFloat("Scale", &scale, 1, 100);
875  ImGui::End();
876 
877  Entity entity;
878  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
879  auto path = flutter::DlPathBuilder{}
880  .MoveTo({97.325, 34.818})
881  .CubicCurveTo({98.50862885295136, 34.81812293973836},
882  {99.46822048142015, 33.85863261475589},
883  {99.46822048142015, 32.67499810206613})
884  .CubicCurveTo({99.46822048142015, 31.491363589376355},
885  {98.50862885295136, 30.53187326439389},
886  {97.32499434685802, 30.531998226542708})
887  .CubicCurveTo({96.14153655073771, 30.532123170035373},
888  {95.18222070648729, 31.491540299350355},
889  {95.18222070648729, 32.67499810206613})
890  .CubicCurveTo({95.18222070648729, 33.85845590478189},
891  {96.14153655073771, 34.81787303409686},
892  {97.32499434685802, 34.81799797758954})
893  .Close()
894  .TakePath();
895  entity.SetTransform(
896  Matrix::MakeScale({scale, scale, 1.0}).Translate({-90, -20, 0}));
897 
898  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
899 
900  auto contents = std::make_shared<SolidColorContents>(geom.get());
901  contents->SetColor(Color::Red());
902 
903  entity.SetContents(contents);
904  return entity.Render(context, pass);
905  };
906  ASSERT_TRUE(OpenPlaygroundHere(callback));
907 }
908 
909 TEST_P(EntityTest, Filters) {
910  auto bridge = CreateTextureForFixture("bay_bridge.jpg");
911  auto boston = CreateTextureForFixture("boston.jpg");
912  auto kalimba = CreateTextureForFixture("kalimba.jpg");
913  ASSERT_TRUE(bridge && boston && kalimba);
914 
915  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
916  auto fi_bridge = FilterInput::Make(bridge);
917  auto fi_boston = FilterInput::Make(boston);
918  auto fi_kalimba = FilterInput::Make(kalimba);
919 
920  std::shared_ptr<FilterContents> blend0 = ColorFilterContents::MakeBlend(
921  BlendMode::kModulate, {fi_kalimba, fi_boston});
922 
923  auto blend1 = ColorFilterContents::MakeBlend(
925  {FilterInput::Make(blend0), fi_bridge, fi_bridge, fi_bridge});
926 
927  Entity entity;
928  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
929  Matrix::MakeTranslation({500, 300}) *
930  Matrix::MakeScale(Vector2{0.5, 0.5}));
931  entity.SetContents(blend1);
932  return entity.Render(context, pass);
933  };
934  ASSERT_TRUE(OpenPlaygroundHere(callback));
935 }
936 
937 TEST_P(EntityTest, GaussianBlurFilter) {
938  auto boston =
939  CreateTextureForFixture("boston.jpg", /*enable_mipmapping=*/true);
940  ASSERT_TRUE(boston);
941 
942  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
943  const char* input_type_names[] = {"Texture", "Solid Color"};
944  const char* blur_type_names[] = {"Image blur", "Mask blur"};
945  const char* pass_variation_names[] = {"New"};
946  const char* blur_style_names[] = {"Normal", "Solid", "Outer", "Inner"};
947  const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"};
948  const FilterContents::BlurStyle blur_styles[] = {
951  const Entity::TileMode tile_modes[] = {
954 
955  // UI state.
956  static int selected_input_type = 0;
957  static Color input_color = Color::Black();
958  static int selected_blur_type = 0;
959  static int selected_pass_variation = 0;
960  static bool combined_sigma = false;
961  static float blur_amount_coarse[2] = {0, 0};
962  static float blur_amount_fine[2] = {10, 10};
963  static int selected_blur_style = 0;
964  static int selected_tile_mode = 3;
965  static Color cover_color(1, 0, 0, 0.2);
966  static Color bounds_color(0, 1, 0, 0.1);
967  static float offset[2] = {500, 400};
968  static float rotation = 0;
969  static float scale[2] = {0.65, 0.65};
970  static float skew[2] = {0, 0};
971  static float path_rect[4] = {0, 0,
972  static_cast<float>(boston->GetSize().width),
973  static_cast<float>(boston->GetSize().height)};
974 
975  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
976  {
977  ImGui::Combo("Input type", &selected_input_type, input_type_names,
978  sizeof(input_type_names) / sizeof(char*));
979  if (selected_input_type == 0) {
980  ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1);
981  } else {
982  ImGui::ColorEdit4("Input color",
983  reinterpret_cast<float*>(&input_color));
984  }
985  ImGui::Combo("Blur type", &selected_blur_type, blur_type_names,
986  sizeof(blur_type_names) / sizeof(char*));
987  if (selected_blur_type == 0) {
988  ImGui::Combo("Pass variation", &selected_pass_variation,
989  pass_variation_names,
990  sizeof(pass_variation_names) / sizeof(char*));
991  }
992  ImGui::Checkbox("Combined sigma", &combined_sigma);
993  if (combined_sigma) {
994  ImGui::SliderFloat("Sigma (coarse)", blur_amount_coarse, 0, 1000);
995  ImGui::SliderFloat("Sigma (fine)", blur_amount_fine, 0, 10);
996  blur_amount_coarse[1] = blur_amount_coarse[0];
997  blur_amount_fine[1] = blur_amount_fine[0];
998  } else {
999  ImGui::SliderFloat2("Sigma (coarse)", blur_amount_coarse, 0, 1000);
1000  ImGui::SliderFloat2("Sigma (fine)", blur_amount_fine, 0, 10);
1001  }
1002  ImGui::Combo("Blur style", &selected_blur_style, blur_style_names,
1003  sizeof(blur_style_names) / sizeof(char*));
1004  ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names,
1005  sizeof(tile_mode_names) / sizeof(char*));
1006  ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color));
1007  ImGui::ColorEdit4("Bounds color ",
1008  reinterpret_cast<float*>(&bounds_color));
1009  ImGui::SliderFloat2("Translation", offset, 0,
1010  pass.GetRenderTargetSize().width);
1011  ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1012  ImGui::SliderFloat2("Scale", scale, 0, 3);
1013  ImGui::SliderFloat2("Skew", skew, -3, 3);
1014  ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000);
1015  }
1016  ImGui::End();
1017 
1018  auto blur_sigma_x = Sigma{blur_amount_coarse[0] + blur_amount_fine[0]};
1019  auto blur_sigma_y = Sigma{blur_amount_coarse[1] + blur_amount_fine[1]};
1020 
1021  std::shared_ptr<Contents> input;
1022  Size input_size;
1023 
1024  auto input_rect =
1025  Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]);
1026 
1027  std::unique_ptr<Geometry> solid_color_input;
1028  if (selected_input_type == 0) {
1029  auto texture = std::make_shared<TextureContents>();
1030  texture->SetSourceRect(Rect::MakeSize(boston->GetSize()));
1031  texture->SetDestinationRect(input_rect);
1032  texture->SetTexture(boston);
1033  texture->SetOpacity(input_color.alpha);
1034 
1035  input = texture;
1036  input_size = input_rect.GetSize();
1037  } else {
1038  solid_color_input =
1039  Geometry::MakeFillPath(flutter::DlPath::MakeRect(input_rect));
1040  auto fill = std::make_shared<SolidColorContents>(solid_color_input.get());
1041  fill->SetColor(input_color);
1042 
1043  input = fill;
1044  input_size = input_rect.GetSize();
1045  }
1046 
1047  std::shared_ptr<FilterContents> blur;
1048  switch (selected_pass_variation) {
1049  case 0:
1050  blur = std::make_shared<GaussianBlurFilterContents>(
1051  blur_sigma_x.sigma, blur_sigma_y.sigma,
1052  tile_modes[selected_tile_mode], /*bounds=*/std::nullopt,
1053  blur_styles[selected_blur_style],
1054  /*geometry=*/nullptr);
1055  blur->SetInputs({FilterInput::Make(input)});
1056  break;
1057  case 1:
1059  FilterInput::Make(input), blur_sigma_x, blur_sigma_y,
1060  tile_modes[selected_tile_mode],
1061  /*bounds=*/std::nullopt, blur_styles[selected_blur_style]);
1062  break;
1063  };
1064  FML_CHECK(blur);
1065 
1066  auto mask_blur = FilterContents::MakeBorderMaskBlur(
1067  FilterInput::Make(input), blur_sigma_x, blur_sigma_y,
1068  blur_styles[selected_blur_style]);
1069 
1070  auto ctm = Matrix::MakeScale(GetContentScale()) *
1071  Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1072  Matrix::MakeRotationZ(Radians(rotation)) *
1073  Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1074  Matrix::MakeSkew(skew[0], skew[1]) *
1075  Matrix::MakeTranslation(-Point(input_size) / 2);
1076 
1077  auto target_contents = selected_blur_type == 0 ? blur : mask_blur;
1078 
1079  Entity entity;
1080  entity.SetContents(target_contents);
1081  entity.SetTransform(ctm);
1082 
1083  entity.Render(context, pass);
1084 
1085  // Renders a red "cover" rectangle that shows the original position of the
1086  // unfiltered input.
1087  Entity cover_entity;
1088  auto geom = Geometry::MakeFillPath(flutter::DlPath::MakeRect(input_rect));
1089  auto contents = std::make_shared<SolidColorContents>(geom.get());
1090  contents->SetColor(cover_color);
1091  cover_entity.SetContents(std::move(contents));
1092  cover_entity.SetTransform(ctm);
1093  cover_entity.Render(context, pass);
1094 
1095  // Renders a green bounding rect of the target filter.
1096  Entity bounds_entity;
1097  std::optional<Rect> target_contents_coverage =
1098  target_contents->GetCoverage(entity);
1099  if (target_contents_coverage.has_value()) {
1100  std::unique_ptr<Geometry> geom =
1101  Geometry::MakeFillPath(flutter::DlPath::MakeRect(
1102  target_contents->GetCoverage(entity).value()));
1103  auto contents = std::make_shared<SolidColorContents>(geom.get());
1104  contents->SetColor(bounds_color);
1105 
1106  bounds_entity.SetContents(contents);
1107  bounds_entity.SetTransform(Matrix());
1108  bounds_entity.Render(context, pass);
1109  }
1110 
1111  return true;
1112  };
1113  ASSERT_TRUE(OpenPlaygroundHere(callback));
1114 }
1115 
1116 TEST_P(EntityTest, MorphologyFilter) {
1117  auto boston = CreateTextureForFixture("boston.jpg");
1118  ASSERT_TRUE(boston);
1119 
1120  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1121  const char* morphology_type_names[] = {"Dilate", "Erode"};
1122  const FilterContents::MorphType morphology_types[] = {
1124  static Color input_color = Color::Black();
1125  // UI state.
1126  static int selected_morphology_type = 0;
1127  static float radius[2] = {20, 20};
1128  static Color cover_color(1, 0, 0, 0.2);
1129  static Color bounds_color(0, 1, 0, 0.1);
1130  static float offset[2] = {500, 400};
1131  static float rotation = 0;
1132  static float scale[2] = {0.65, 0.65};
1133  static float skew[2] = {0, 0};
1134  static float path_rect[4] = {0, 0,
1135  static_cast<float>(boston->GetSize().width),
1136  static_cast<float>(boston->GetSize().height)};
1137  static float effect_transform_scale = 1;
1138 
1139  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1140  {
1141  ImGui::Combo("Morphology type", &selected_morphology_type,
1142  morphology_type_names,
1143  sizeof(morphology_type_names) / sizeof(char*));
1144  ImGui::SliderFloat2("Radius", radius, 0, 200);
1145  ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1);
1146  ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color));
1147  ImGui::ColorEdit4("Bounds color ",
1148  reinterpret_cast<float*>(&bounds_color));
1149  ImGui::SliderFloat2("Translation", offset, 0,
1150  pass.GetRenderTargetSize().width);
1151  ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1152  ImGui::SliderFloat2("Scale", scale, 0, 3);
1153  ImGui::SliderFloat2("Skew", skew, -3, 3);
1154  ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000);
1155  ImGui::SliderFloat("Effect transform scale", &effect_transform_scale, 0,
1156  3);
1157  }
1158  ImGui::End();
1159 
1160  std::shared_ptr<Contents> input;
1161  Size input_size;
1162 
1163  auto input_rect =
1164  Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]);
1165  auto texture = std::make_shared<TextureContents>();
1166  texture->SetSourceRect(Rect::MakeSize(boston->GetSize()));
1167  texture->SetDestinationRect(input_rect);
1168  texture->SetTexture(boston);
1169  texture->SetOpacity(input_color.alpha);
1170 
1171  input = texture;
1172  input_size = input_rect.GetSize();
1173 
1174  auto contents = FilterContents::MakeMorphology(
1175  FilterInput::Make(input), Radius{radius[0]}, Radius{radius[1]},
1176  morphology_types[selected_morphology_type]);
1177  contents->SetEffectTransform(Matrix::MakeScale(
1178  Vector2{effect_transform_scale, effect_transform_scale}));
1179 
1180  auto ctm = Matrix::MakeScale(GetContentScale()) *
1181  Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1182  Matrix::MakeRotationZ(Radians(rotation)) *
1183  Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1184  Matrix::MakeSkew(skew[0], skew[1]) *
1185  Matrix::MakeTranslation(-Point(input_size) / 2);
1186 
1187  Entity entity;
1188  entity.SetContents(contents);
1189  entity.SetTransform(ctm);
1190 
1191  entity.Render(context, pass);
1192 
1193  // Renders a red "cover" rectangle that shows the original position of the
1194  // unfiltered input.
1195  Entity cover_entity;
1196  auto geom = Geometry::MakeFillPath(flutter::DlPath::MakeRect(input_rect));
1197  auto cover_contents = std::make_shared<SolidColorContents>(geom.get());
1198  cover_contents->SetColor(cover_color);
1199  cover_entity.SetContents(cover_contents);
1200  cover_entity.SetTransform(ctm);
1201  cover_entity.Render(context, pass);
1202 
1203  // Renders a green bounding rect of the target filter.
1204  Entity bounds_entity;
1205  std::unique_ptr<Geometry> bounds_geom = Geometry::MakeFillPath(
1206  flutter::DlPath::MakeRect(contents->GetCoverage(entity).value()));
1207  auto bounds_contents =
1208  std::make_shared<SolidColorContents>(bounds_geom.get());
1209  bounds_contents->SetColor(bounds_color);
1210  bounds_entity.SetContents(std::move(bounds_contents));
1211  bounds_entity.SetTransform(Matrix());
1212 
1213  bounds_entity.Render(context, pass);
1214 
1215  return true;
1216  };
1217  ASSERT_TRUE(OpenPlaygroundHere(callback));
1218 }
1219 
1220 TEST_P(EntityTest, SetBlendMode) {
1221  Entity entity;
1222  ASSERT_EQ(entity.GetBlendMode(), BlendMode::kSrcOver);
1224  ASSERT_EQ(entity.GetBlendMode(), BlendMode::kClear);
1225 }
1226 
1227 TEST_P(EntityTest, ContentsGetBoundsForEmptyPathReturnsNullopt) {
1228  Entity entity;
1229  entity.SetContents(std::make_shared<SolidColorContents>(nullptr));
1230  ASSERT_FALSE(entity.GetCoverage().has_value());
1231 }
1232 
1233 TEST(EntityTest, UberSDFContentsCoverageFillRect) {
1234  auto rect = Rect::MakeXYWH(100, 100, 200, 200);
1235  auto params =
1236  UberSDFParameters::MakeRect(Color::Red(), rect, /*stroke=*/std::nullopt);
1237  auto geometry = std::make_unique<UberSDFGeometry>(params);
1238  auto contents = UberSDFContents::Make(params, std::move(geometry));
1239 
1240  Entity entity;
1241  auto coverage = contents->GetCoverage(entity);
1242  ASSERT_TRUE(coverage.has_value());
1244  coverage.value(),
1245  Rect::MakeXYWH(100, 100, 200, 200).Expand(1.0f)); // expanded by AA
1246 }
1247 
1248 TEST(EntityTest, UberSDFContentsCoverageStrokeRect) {
1249  auto rect = Rect::MakeXYWH(100, 100, 200, 200);
1250  auto params = UberSDFParameters::MakeRect(Color::Red(), rect,
1251  StrokeParameters{.width = 4.0f});
1252  auto geometry = std::make_unique<UberSDFGeometry>(params);
1253  auto contents = UberSDFContents::Make(params, std::move(geometry));
1254 
1255  Entity entity;
1256  auto coverage = contents->GetCoverage(entity);
1257  ASSERT_TRUE(coverage.has_value());
1258  ASSERT_RECT_NEAR(coverage.value(),
1259  Rect::MakeXYWH(100, 100, 200, 200)
1260  .Expand(3.0f)); // expanded by half stroke width + AA
1261 }
1262 
1263 TEST(EntityTest, UberSDFContentsCoverageFillCircle) {
1264  auto params =
1265  UberSDFParameters::MakeCircle(Color::Red(), /*center=*/{50, 50},
1266  /*radius=*/10.0f, /*stroke=*/std::nullopt);
1267  auto geometry = std::make_unique<UberSDFGeometry>(params);
1268  auto contents = UberSDFContents::Make(params, std::move(geometry));
1269 
1270  Entity entity;
1271  auto coverage = contents->GetCoverage(entity);
1272  ASSERT_TRUE(coverage.has_value());
1274  coverage.value(),
1275  Rect::MakeXYWH(40, 40, 20, 20).Expand(1.0f)); // expanded by AA
1276 }
1277 
1278 TEST(EntityTest, UberSDFContentsCoverageStrokeCircle) {
1279  auto params = UberSDFParameters::MakeCircle(Color::Red(), /*center=*/{50, 50},
1280  /*radius=*/10.0f,
1281  StrokeParameters{.width = 4.0f});
1282  auto geometry = std::make_unique<UberSDFGeometry>(params);
1283  auto contents = UberSDFContents::Make(params, std::move(geometry));
1284 
1285  Entity entity;
1286  auto coverage = contents->GetCoverage(entity);
1287  ASSERT_TRUE(coverage.has_value());
1288  ASSERT_RECT_NEAR(coverage.value(),
1289  Rect::MakeXYWH(40, 40, 20, 20)
1290  .Expand(3.0f)); // expanded by half stroke width + AA
1291 }
1292 
1293 TEST_P(EntityTest, SolidStrokeCoverageIsCorrect) {
1294  {
1295  auto geometry = Geometry::MakeStrokePath(
1296  flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1297  {
1298  .width = 4.0f,
1299  .cap = Cap::kButt,
1300  .join = Join::kBevel,
1301  .miter_limit = 4.0f,
1302  });
1303 
1304  Entity entity;
1305  auto contents = std::make_unique<SolidColorContents>(geometry.get());
1306  contents->SetColor(Color::Black());
1307  entity.SetContents(std::move(contents));
1308  auto actual = entity.GetCoverage();
1309  auto expected = Rect::MakeLTRB(-2, -2, 12, 12);
1310 
1311  ASSERT_TRUE(actual.has_value());
1312  ASSERT_RECT_NEAR(actual.value(), expected);
1313  }
1314 
1315  // Cover the Cap::kSquare case.
1316  {
1317  auto geometry = Geometry::MakeStrokePath(
1318  flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1319  {
1320  .width = 4.0,
1321  .cap = Cap::kSquare,
1322  .join = Join::kBevel,
1323  .miter_limit = 4.0,
1324  });
1325 
1326  Entity entity;
1327  auto contents = std::make_unique<SolidColorContents>(geometry.get());
1328  contents->SetColor(Color::Black());
1329  entity.SetContents(std::move(contents));
1330  auto actual = entity.GetCoverage();
1331  auto expected =
1332  Rect::MakeLTRB(-sqrt(8), -sqrt(8), 10 + sqrt(8), 10 + sqrt(8));
1333 
1334  ASSERT_TRUE(actual.has_value());
1335  ASSERT_RECT_NEAR(actual.value(), expected);
1336  }
1337 
1338  // Cover the Join::kMiter case.
1339  {
1340  auto geometry = Geometry::MakeStrokePath(
1341  flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1342  {
1343  .width = 4.0f,
1344  .cap = Cap::kSquare,
1345  .join = Join::kMiter,
1346  .miter_limit = 2.0f,
1347  });
1348 
1349  Entity entity;
1350  auto contents = std::make_unique<SolidColorContents>(geometry.get());
1351  contents->SetColor(Color::Black());
1352  entity.SetContents(std::move(contents));
1353  auto actual = entity.GetCoverage();
1354  auto expected = Rect::MakeLTRB(-4, -4, 14, 14);
1355 
1356  ASSERT_TRUE(actual.has_value());
1357  ASSERT_RECT_NEAR(actual.value(), expected);
1358  }
1359 }
1360 
1361 TEST_P(EntityTest, BorderMaskBlurCoverageIsCorrect) {
1362  auto geom = Geometry::MakeFillPath(
1363  flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1364  auto fill = std::make_shared<SolidColorContents>(geom.get());
1365  fill->SetColor(Color::CornflowerBlue());
1366  auto border_mask_blur = FilterContents::MakeBorderMaskBlur(
1367  FilterInput::Make(fill), Radius{3}, Radius{4});
1368 
1369  {
1370  Entity e;
1371  e.SetTransform(Matrix());
1372  auto actual = border_mask_blur->GetCoverage(e);
1373  auto expected = Rect::MakeXYWH(-3, -4, 306, 408);
1374  ASSERT_TRUE(actual.has_value());
1375  ASSERT_RECT_NEAR(actual.value(), expected);
1376  }
1377 
1378  {
1379  Entity e;
1381  auto actual = border_mask_blur->GetCoverage(e);
1382  auto expected = Rect::MakeXYWH(-287.792, -4.94975, 504.874, 504.874);
1383  ASSERT_TRUE(actual.has_value());
1384  ASSERT_RECT_NEAR(actual.value(), expected);
1385  }
1386 }
1387 
1388 TEST_P(EntityTest, SolidFillCoverageIsCorrect) {
1389  // No transform
1390  {
1391  auto expected = Rect::MakeLTRB(100, 110, 200, 220);
1392  auto geom = Geometry::MakeFillPath(flutter::DlPath::MakeRect(expected));
1393  auto fill = std::make_shared<SolidColorContents>(geom.get());
1394  fill->SetColor(Color::CornflowerBlue());
1395 
1396  auto coverage = fill->GetCoverage({});
1397  ASSERT_TRUE(coverage.has_value());
1398  ASSERT_RECT_NEAR(coverage.value(), expected);
1399  }
1400 
1401  // Entity transform
1402  {
1403  auto geom = Geometry::MakeFillPath(
1404  flutter::DlPath::MakeRect(Rect::MakeLTRB(100, 110, 200, 220)));
1405  auto fill = std::make_shared<SolidColorContents>(geom.get());
1406  fill->SetColor(Color::CornflowerBlue());
1407 
1408  Entity entity;
1410  entity.SetContents(std::move(fill));
1411 
1412  auto coverage = entity.GetCoverage();
1413  auto expected = Rect::MakeLTRB(104, 115, 204, 225);
1414  ASSERT_TRUE(coverage.has_value());
1415  ASSERT_RECT_NEAR(coverage.value(), expected);
1416  }
1417 
1418  // No coverage for fully transparent colors
1419  {
1420  auto geom = Geometry::MakeFillPath(
1421  flutter::DlPath::MakeRect(Rect::MakeLTRB(100, 110, 200, 220)));
1422  auto fill = std::make_shared<SolidColorContents>(geom.get());
1423  fill->SetColor(Color::WhiteTransparent());
1424 
1425  auto coverage = fill->GetCoverage({});
1426  ASSERT_FALSE(coverage.has_value());
1427  }
1428 }
1429 
1430 TEST_P(EntityTest, RRectShadowTest) {
1431  auto callback = [&](ContentContext& context, RenderPass& pass) {
1432  static Color color = Color::Red();
1433  static float corner_radius = 100;
1434  static float blur_radius = 100;
1435  static bool show_coverage = false;
1436  static Color coverage_color = Color::Green().WithAlpha(0.2);
1437  static PlaygroundPoint top_left_point(Point(200, 200), 30, Color::White());
1438  static PlaygroundPoint bottom_right_point(Point(600, 400), 30,
1439  Color::White());
1440 
1441  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1442  ImGui::SliderFloat("Corner radius", &corner_radius, 0, 300);
1443  ImGui::SliderFloat("Blur radius", &blur_radius, 0, 300);
1444  ImGui::ColorEdit4("Color", reinterpret_cast<Scalar*>(&color));
1445  ImGui::Checkbox("Show coverage", &show_coverage);
1446  if (show_coverage) {
1447  ImGui::ColorEdit4("Coverage color",
1448  reinterpret_cast<Scalar*>(&coverage_color));
1449  }
1450  ImGui::End();
1451 
1452  auto [top_left, bottom_right] =
1453  DrawPlaygroundLine(top_left_point, bottom_right_point);
1454  auto rect =
1455  Rect::MakeLTRB(top_left.x, top_left.y, bottom_right.x, bottom_right.y);
1456 
1457  auto contents = std::make_unique<SolidRRectBlurContents>();
1458  contents->SetShape(rect, corner_radius);
1459  contents->SetColor(color);
1460  contents->SetSigma(Radius(blur_radius));
1461 
1462  Entity entity;
1463  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
1464  entity.SetContents(std::move(contents));
1465  entity.Render(context, pass);
1466 
1467  auto coverage = entity.GetCoverage();
1468  if (show_coverage && coverage.has_value()) {
1469  auto geom = Geometry::MakeFillPath(
1470  flutter::DlPath::MakeRect(entity.GetCoverage().value()));
1471  auto bounds_contents = std::make_unique<SolidColorContents>(geom.get());
1472  bounds_contents->SetColor(coverage_color.Premultiply());
1473  Entity bounds_entity;
1474  bounds_entity.SetContents(std::move(bounds_contents));
1475  bounds_entity.Render(context, pass);
1476  }
1477 
1478  return true;
1479  };
1480  ASSERT_TRUE(OpenPlaygroundHere(callback));
1481 }
1482 
1483 TEST_P(EntityTest, ColorMatrixFilterCoverageIsCorrect) {
1484  // Set up a simple color background.
1485  auto geom = Geometry::MakeFillPath(
1486  flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1487  auto fill = std::make_shared<SolidColorContents>(geom.get());
1488  fill->SetColor(Color::Coral());
1489 
1490  // Set the color matrix filter.
1491  ColorMatrix matrix = {
1492  1, 1, 1, 1, 1, //
1493  1, 1, 1, 1, 1, //
1494  1, 1, 1, 1, 1, //
1495  1, 1, 1, 1, 1, //
1496  };
1497 
1498  auto filter =
1500 
1501  Entity e;
1502  e.SetTransform(Matrix());
1503 
1504  // Confirm that the actual filter coverage matches the expected coverage.
1505  auto actual = filter->GetCoverage(e);
1506  auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1507 
1508  ASSERT_TRUE(actual.has_value());
1509  ASSERT_RECT_NEAR(actual.value(), expected);
1510 }
1511 
1512 TEST_P(EntityTest, ColorMatrixFilterEditable) {
1513  auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg");
1514  ASSERT_TRUE(bay_bridge);
1515 
1516  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1517  // UI state.
1518  static ColorMatrix color_matrix = {
1519  1, 0, 0, 0, 0, //
1520  0, 3, 0, 0, 0, //
1521  0, 0, 1, 0, 0, //
1522  0, 0, 0, 1, 0, //
1523  };
1524  static float offset[2] = {500, 400};
1525  static float rotation = 0;
1526  static float scale[2] = {0.65, 0.65};
1527  static float skew[2] = {0, 0};
1528 
1529  // Define the ImGui
1530  ImGui::Begin("Color Matrix", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1531  {
1532  std::string label = "##1";
1533  for (int i = 0; i < 20; i += 5) {
1534  ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float,
1535  &(color_matrix.array[i]), 5, nullptr, nullptr,
1536  "%.2f", 0);
1537  label[2]++;
1538  }
1539 
1540  ImGui::SliderFloat2("Translation", &offset[0], 0,
1541  pass.GetRenderTargetSize().width);
1542  ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1543  ImGui::SliderFloat2("Scale", &scale[0], 0, 3);
1544  ImGui::SliderFloat2("Skew", &skew[0], -3, 3);
1545  }
1546  ImGui::End();
1547 
1548  // Set the color matrix filter.
1550  FilterInput::Make(bay_bridge), color_matrix);
1551 
1552  // Define the entity with the color matrix filter.
1553  Entity entity;
1554  entity.SetTransform(
1555  Matrix::MakeScale(GetContentScale()) *
1556  Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1557  Matrix::MakeRotationZ(Radians(rotation)) *
1558  Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1559  Matrix::MakeSkew(skew[0], skew[1]) *
1560  Matrix::MakeTranslation(-Point(bay_bridge->GetSize()) / 2));
1561  entity.SetContents(filter);
1562  entity.Render(context, pass);
1563 
1564  return true;
1565  };
1566 
1567  ASSERT_TRUE(OpenPlaygroundHere(callback));
1568 }
1569 
1570 TEST_P(EntityTest, LinearToSrgbFilterCoverageIsCorrect) {
1571  // Set up a simple color background.
1572  auto geom = Geometry::MakeFillPath(
1573  flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1574  auto fill = std::make_shared<SolidColorContents>(geom.get());
1575  fill->SetColor(Color::MintCream());
1576 
1577  auto filter =
1579 
1580  Entity e;
1581  e.SetTransform(Matrix());
1582 
1583  // Confirm that the actual filter coverage matches the expected coverage.
1584  auto actual = filter->GetCoverage(e);
1585  auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1586 
1587  ASSERT_TRUE(actual.has_value());
1588  ASSERT_RECT_NEAR(actual.value(), expected);
1589 }
1590 
1591 TEST_P(EntityTest, LinearToSrgbFilter) {
1592  auto image = CreateTextureForFixture("kalimba.jpg");
1593  ASSERT_TRUE(image);
1594 
1595  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1596  auto filtered =
1598 
1599  // Define the entity that will serve as the control image as a Gaussian blur
1600  // filter with no filter at all.
1601  Entity entity_left;
1602  entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) *
1603  Matrix::MakeTranslation({100, 300}) *
1604  Matrix::MakeScale(Vector2{0.5, 0.5}));
1605  auto unfiltered = FilterContents::MakeGaussianBlur(FilterInput::Make(image),
1606  Sigma{0}, Sigma{0});
1607  entity_left.SetContents(unfiltered);
1608 
1609  // Define the entity that will be filtered from linear to sRGB.
1610  Entity entity_right;
1611  entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) *
1612  Matrix::MakeTranslation({500, 300}) *
1613  Matrix::MakeScale(Vector2{0.5, 0.5}));
1614  entity_right.SetContents(filtered);
1615  return entity_left.Render(context, pass) &&
1616  entity_right.Render(context, pass);
1617  };
1618 
1619  ASSERT_TRUE(OpenPlaygroundHere(callback));
1620 }
1621 
1622 TEST_P(EntityTest, SrgbToLinearFilterCoverageIsCorrect) {
1623  // Set up a simple color background.
1624  auto geom = Geometry::MakeFillPath(
1625  flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1626  auto fill = std::make_shared<SolidColorContents>(geom.get());
1627  fill->SetColor(Color::DeepPink());
1628 
1629  auto filter =
1631 
1632  Entity e;
1633  e.SetTransform(Matrix());
1634 
1635  // Confirm that the actual filter coverage matches the expected coverage.
1636  auto actual = filter->GetCoverage(e);
1637  auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1638 
1639  ASSERT_TRUE(actual.has_value());
1640  ASSERT_RECT_NEAR(actual.value(), expected);
1641 }
1642 
1643 TEST_P(EntityTest, SrgbToLinearFilter) {
1644  auto image = CreateTextureForFixture("embarcadero.jpg");
1645  ASSERT_TRUE(image);
1646 
1647  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1648  auto filtered =
1650 
1651  // Define the entity that will serve as the control image as a Gaussian blur
1652  // filter with no filter at all.
1653  Entity entity_left;
1654  entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) *
1655  Matrix::MakeTranslation({100, 300}) *
1656  Matrix::MakeScale(Vector2{0.5, 0.5}));
1657  auto unfiltered = FilterContents::MakeGaussianBlur(FilterInput::Make(image),
1658  Sigma{0}, Sigma{0});
1659  entity_left.SetContents(unfiltered);
1660 
1661  // Define the entity that will be filtered from sRGB to linear.
1662  Entity entity_right;
1663  entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) *
1664  Matrix::MakeTranslation({500, 300}) *
1665  Matrix::MakeScale(Vector2{0.5, 0.5}));
1666  entity_right.SetContents(filtered);
1667  return entity_left.Render(context, pass) &&
1668  entity_right.Render(context, pass);
1669  };
1670 
1671  ASSERT_TRUE(OpenPlaygroundHere(callback));
1672 }
1673 
1674 static Vector3 RGBToYUV(Vector3 rgb, YUVColorSpace yuv_color_space) {
1675  Vector3 yuv;
1676  switch (yuv_color_space) {
1678  yuv.x = rgb.x * 0.299 + rgb.y * 0.587 + rgb.z * 0.114;
1679  yuv.y = rgb.x * -0.169 + rgb.y * -0.331 + rgb.z * 0.5 + 0.5;
1680  yuv.z = rgb.x * 0.5 + rgb.y * -0.419 + rgb.z * -0.081 + 0.5;
1681  break;
1683  yuv.x = rgb.x * 0.257 + rgb.y * 0.516 + rgb.z * 0.100 + 0.063;
1684  yuv.y = rgb.x * -0.145 + rgb.y * -0.291 + rgb.z * 0.439 + 0.5;
1685  yuv.z = rgb.x * 0.429 + rgb.y * -0.368 + rgb.z * -0.071 + 0.5;
1686  break;
1687  }
1688  return yuv;
1689 }
1690 
1691 static std::vector<std::shared_ptr<Texture>> CreateTestYUVTextures(
1692  Context* context,
1693  YUVColorSpace yuv_color_space) {
1694  Vector3 red = {244.0 / 255.0, 67.0 / 255.0, 54.0 / 255.0};
1695  Vector3 green = {76.0 / 255.0, 175.0 / 255.0, 80.0 / 255.0};
1696  Vector3 blue = {33.0 / 255.0, 150.0 / 255.0, 243.0 / 255.0};
1697  Vector3 white = {1.0, 1.0, 1.0};
1698  Vector3 red_yuv = RGBToYUV(red, yuv_color_space);
1699  Vector3 green_yuv = RGBToYUV(green, yuv_color_space);
1700  Vector3 blue_yuv = RGBToYUV(blue, yuv_color_space);
1701  Vector3 white_yuv = RGBToYUV(white, yuv_color_space);
1702  std::vector<Vector3> yuvs{red_yuv, green_yuv, blue_yuv, white_yuv};
1703  std::vector<uint8_t> y_data;
1704  std::vector<uint8_t> uv_data;
1705  for (int i = 0; i < 4; i++) {
1706  auto yuv = yuvs[i];
1707  uint8_t y = std::round(yuv.x * 255.0);
1708  uint8_t u = std::round(yuv.y * 255.0);
1709  uint8_t v = std::round(yuv.z * 255.0);
1710  for (int j = 0; j < 16; j++) {
1711  y_data.push_back(y);
1712  }
1713  for (int j = 0; j < 8; j++) {
1714  uv_data.push_back(j % 2 == 0 ? u : v);
1715  }
1716  }
1717  auto cmd_buffer = context->CreateCommandBuffer();
1718  auto blit_pass = cmd_buffer->CreateBlitPass();
1719 
1720  impeller::TextureDescriptor y_texture_descriptor;
1721  y_texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
1722  y_texture_descriptor.format = PixelFormat::kR8UNormInt;
1723  y_texture_descriptor.size = {8, 8};
1724  auto y_texture =
1725  context->GetResourceAllocator()->CreateTexture(y_texture_descriptor);
1726  auto y_mapping = std::make_shared<fml::DataMapping>(y_data);
1727  auto y_mapping_buffer =
1728  context->GetResourceAllocator()->CreateBufferWithCopy(*y_mapping);
1729 
1730  blit_pass->AddCopy(DeviceBuffer::AsBufferView(y_mapping_buffer), y_texture);
1731 
1732  impeller::TextureDescriptor uv_texture_descriptor;
1733  uv_texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
1734  uv_texture_descriptor.format = PixelFormat::kR8G8UNormInt;
1735  uv_texture_descriptor.size = {4, 4};
1736  auto uv_texture =
1737  context->GetResourceAllocator()->CreateTexture(uv_texture_descriptor);
1738  auto uv_mapping = std::make_shared<fml::DataMapping>(uv_data);
1739  auto uv_mapping_buffer =
1740  context->GetResourceAllocator()->CreateBufferWithCopy(*uv_mapping);
1741 
1742  blit_pass->AddCopy(DeviceBuffer::AsBufferView(uv_mapping_buffer), uv_texture);
1743 
1744  if (!blit_pass->EncodeCommands() ||
1745  !context->GetCommandQueue()->Submit({cmd_buffer}).ok()) {
1746  FML_DLOG(ERROR) << "Could not copy contents into Y/UV texture.";
1747  }
1748 
1749  return {y_texture, uv_texture};
1750 }
1751 
1752 TEST_P(EntityTest, YUVToRGBFilter) {
1753  if (GetParam() == PlaygroundBackend::kOpenGLES ||
1754  GetParam() == PlaygroundBackend::kOpenGLESSDF) {
1755  // TODO(114588) : Support YUV to RGB filter on OpenGLES backend.
1756  GTEST_SKIP()
1757  << "YUV to RGB filter is not supported on OpenGLES backend yet.";
1758  }
1759 
1760  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1761  YUVColorSpace yuv_color_space_array[2]{YUVColorSpace::kBT601FullRange,
1763  for (int i = 0; i < 2; i++) {
1764  auto yuv_color_space = yuv_color_space_array[i];
1765  auto textures =
1766  CreateTestYUVTextures(GetContext().get(), yuv_color_space);
1767  auto filter_contents = FilterContents::MakeYUVToRGBFilter(
1768  textures[0], textures[1], yuv_color_space);
1769  Entity filter_entity;
1770  filter_entity.SetContents(filter_contents);
1771  auto snapshot =
1772  filter_contents->RenderToSnapshot(context, filter_entity, {});
1773 
1774  Entity entity;
1775  auto contents = TextureContents::MakeRect(Rect::MakeLTRB(0, 0, 256, 256));
1776  contents->SetTexture(snapshot->texture);
1777  contents->SetSourceRect(Rect::MakeSize(snapshot->texture->GetSize()));
1778  entity.SetContents(contents);
1779  entity.SetTransform(
1780  Matrix::MakeTranslation({static_cast<Scalar>(100 + 400 * i), 300}));
1781  entity.Render(context, pass);
1782  }
1783  return true;
1784  };
1785  ASSERT_TRUE(OpenPlaygroundHere(callback));
1786 }
1787 
1788 TEST_P(EntityTest, RuntimeEffect) {
1789  auto runtime_stages_result =
1790  OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1791  ABSL_ASSERT_OK(runtime_stages_result);
1792  std::shared_ptr<RuntimeStage> runtime_stage =
1793  runtime_stages_result.value()[GetRuntimeStageBackend()];
1794  ASSERT_TRUE(runtime_stage);
1795  ASSERT_TRUE(runtime_stage->IsDirty());
1796 
1797  bool expect_dirty = true;
1798 
1799  PipelineRef first_pipeline;
1800  std::unique_ptr<Geometry> geom = Geometry::MakeCover();
1801 
1802  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1803  EXPECT_EQ(runtime_stage->IsDirty(), expect_dirty);
1804 
1805  auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1806  contents->SetRuntimeStage(runtime_stage);
1807 
1808  struct FragUniforms {
1809  Vector2 iResolution;
1810  Scalar iTime;
1811  } frag_uniforms = {
1812  .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1813  .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1814  };
1815  auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1816  uniform_data->resize(sizeof(FragUniforms));
1817  memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1818  contents->SetUniformData(uniform_data);
1819 
1820  Entity entity;
1821  entity.SetContents(contents);
1822  bool result = contents->Render(context, entity, pass);
1823 
1824  if (expect_dirty) {
1825  first_pipeline = pass.GetCommands().back().pipeline;
1826  } else {
1827  EXPECT_EQ(pass.GetCommands().back().pipeline, first_pipeline);
1828  }
1829  expect_dirty = false;
1830  return result;
1831  };
1832 
1833  // Simulate some renders and hot reloading of the shader.
1834  ContentContext& content_context = GetContentContext();
1835  {
1836  RenderTarget target =
1837  content_context.GetRenderTargetCache()->CreateOffscreen(
1838  *content_context.GetContext(), {1, 1}, 1u);
1839 
1840  testing::MockRenderPass mock_pass(GetContext(), target);
1841  callback(content_context, mock_pass);
1842  callback(content_context, mock_pass);
1843 
1844  // Dirty the runtime stage.
1845  auto runtime_stages_result =
1846  OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1847  ABSL_ASSERT_OK(runtime_stages_result);
1848  runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1849 
1850  ASSERT_TRUE(runtime_stage->IsDirty());
1851  expect_dirty = true;
1852 
1853  callback(content_context, mock_pass);
1854  }
1855 }
1856 
1857 TEST_P(EntityTest, RuntimeEffectCanSuccessfullyRender) {
1858  auto runtime_stages_result =
1859  OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1860  ABSL_ASSERT_OK(runtime_stages_result);
1861  auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1862  ASSERT_TRUE(runtime_stage);
1863  ASSERT_TRUE(runtime_stage->IsDirty());
1864 
1865  auto geom = Geometry::MakeCover();
1866  auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1867  contents->SetRuntimeStage(runtime_stage);
1868 
1869  struct FragUniforms {
1870  Vector2 iResolution;
1871  Scalar iTime;
1872  } frag_uniforms = {
1873  .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1874  .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1875  };
1876  auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1877  uniform_data->resize(sizeof(FragUniforms));
1878  memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1879  contents->SetUniformData(uniform_data);
1880 
1881  Entity entity;
1882  entity.SetContents(contents);
1883 
1884  // Create a render target with a depth-stencil, similar to how EntityPass
1885  // does.
1886  RenderTarget target =
1887  GetContentContext().GetRenderTargetCache()->CreateOffscreenMSAA(
1888  *GetContext(), {GetWindowSize().width, GetWindowSize().height}, 1,
1889  "RuntimeEffect Texture");
1890  testing::MockRenderPass pass(GetContext(), target);
1891 
1892  ASSERT_TRUE(contents->Render(GetContentContext(), entity, pass));
1893  ASSERT_EQ(pass.GetCommands().size(), 1u);
1894  const auto& command = pass.GetCommands()[0];
1895  ASSERT_TRUE(command.pipeline->GetDescriptor()
1896  .GetDepthStencilAttachmentDescriptor()
1897  .has_value());
1898  ASSERT_TRUE(command.pipeline->GetDescriptor()
1899  .GetFrontStencilAttachmentDescriptor()
1900  .has_value());
1901 }
1902 
1903 TEST_P(EntityTest, RuntimeEffectCanPrecache) {
1904  auto runtime_stages_result =
1905  OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1906  ABSL_ASSERT_OK(runtime_stages_result);
1907  std::shared_ptr<RuntimeStage> runtime_stage =
1908  runtime_stages_result.value()[GetRuntimeStageBackend()];
1909  ASSERT_TRUE(runtime_stage);
1910  ASSERT_TRUE(runtime_stage->IsDirty());
1911 
1912  auto geom = Geometry::MakeCover();
1913  auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1914  contents->SetRuntimeStage(runtime_stage);
1915 
1916  EXPECT_TRUE(contents->BootstrapShader(GetContentContext()));
1917 }
1918 
1919 TEST_P(EntityTest, RuntimeEffectSetsRightSizeWhenUniformIsStruct) {
1920  if (GetBackend() != PlaygroundBackend::kVulkan) {
1921  GTEST_SKIP() << "Test only applies to Vulkan";
1922  }
1923 
1924  auto runtime_stages_result =
1925  OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1926  ABSL_ASSERT_OK(runtime_stages_result);
1927  auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1928  ASSERT_TRUE(runtime_stage);
1929  ASSERT_TRUE(runtime_stage->IsDirty());
1930 
1931  auto geom = Geometry::MakeCover();
1932  auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1933  contents->SetRuntimeStage(runtime_stage);
1934 
1935  struct FragUniforms {
1936  Vector2 iResolution;
1937  Scalar iTime;
1938  } frag_uniforms = {
1939  .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1940  .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1941  };
1942  auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1943  uniform_data->resize(sizeof(FragUniforms));
1944  memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1945 
1946  auto buffer_view = RuntimeEffectContents::EmplaceUniform(
1947  uniform_data->data(), GetContentContext().GetTransientsDataBuffer(),
1948  runtime_stage->GetUniforms()[0]);
1949 
1950  // 16 bytes:
1951  // 8 bytes for iResolution
1952  // 4 bytes for iTime
1953  // 4 bytes padding
1954  EXPECT_EQ(buffer_view.GetRange().length, 16u);
1955 }
1956 
1957 TEST_P(EntityTest, ColorFilterWithForegroundColorAdvancedBlend) {
1958  auto image = CreateTextureForFixture("boston.jpg");
1959  auto filter = ColorFilterContents::MakeBlend(
1961 
1962  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1963  Entity entity;
1964  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1965  Matrix::MakeTranslation({500, 300}) *
1966  Matrix::MakeScale(Vector2{0.5, 0.5}));
1967  entity.SetContents(filter);
1968  return entity.Render(context, pass);
1969  };
1970  ASSERT_TRUE(OpenPlaygroundHere(callback));
1971 }
1972 
1973 TEST_P(EntityTest, ColorFilterWithForegroundColorClearBlend) {
1974  auto image = CreateTextureForFixture("boston.jpg");
1975  auto filter = ColorFilterContents::MakeBlend(
1977 
1978  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1979  Entity entity;
1980  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1981  Matrix::MakeTranslation({500, 300}) *
1982  Matrix::MakeScale(Vector2{0.5, 0.5}));
1983  entity.SetContents(filter);
1984  return entity.Render(context, pass);
1985  };
1986  ASSERT_TRUE(OpenPlaygroundHere(callback));
1987 }
1988 
1989 TEST_P(EntityTest, ColorFilterWithForegroundColorSrcBlend) {
1990  auto image = CreateTextureForFixture("boston.jpg");
1991  auto filter = ColorFilterContents::MakeBlend(
1993 
1994  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1995  Entity entity;
1996  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1997  Matrix::MakeTranslation({500, 300}) *
1998  Matrix::MakeScale(Vector2{0.5, 0.5}));
1999  entity.SetContents(filter);
2000  return entity.Render(context, pass);
2001  };
2002  ASSERT_TRUE(OpenPlaygroundHere(callback));
2003 }
2004 
2005 TEST_P(EntityTest, ColorFilterWithForegroundColorDstBlend) {
2006  auto image = CreateTextureForFixture("boston.jpg");
2007  auto filter = ColorFilterContents::MakeBlend(
2009 
2010  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2011  Entity entity;
2012  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2013  Matrix::MakeTranslation({500, 300}) *
2014  Matrix::MakeScale(Vector2{0.5, 0.5}));
2015  entity.SetContents(filter);
2016  return entity.Render(context, pass);
2017  };
2018  ASSERT_TRUE(OpenPlaygroundHere(callback));
2019 }
2020 
2021 TEST_P(EntityTest, ColorFilterWithForegroundColorSrcInBlend) {
2022  auto image = CreateTextureForFixture("boston.jpg");
2023  auto filter = ColorFilterContents::MakeBlend(
2025 
2026  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2027  Entity entity;
2028  entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2029  Matrix::MakeTranslation({500, 300}) *
2030  Matrix::MakeScale(Vector2{0.5, 0.5}));
2031  entity.SetContents(filter);
2032  return entity.Render(context, pass);
2033  };
2034  ASSERT_TRUE(OpenPlaygroundHere(callback));
2035 }
2036 
2037 TEST_P(EntityTest, CoverageForStrokePathWithNegativeValuesInTransform) {
2038  auto arrow_head = flutter::DlPathBuilder{}
2039  .MoveTo({50, 120})
2040  .LineTo({120, 190})
2041  .LineTo({190, 120})
2042  .TakePath();
2043  auto geometry = Geometry::MakeStrokePath(arrow_head, //
2044  {
2045  .width = 15.0f,
2046  .cap = Cap::kRound,
2047  .join = Join::kRound,
2048  .miter_limit = 4.0f,
2049  });
2050 
2051  auto transform = Matrix::MakeTranslation({300, 300}) *
2053  // Note that e[0][0] used to be tested here, but it was -epsilon solely
2054  // due to floating point inaccuracy in the transcendental trig functions.
2055  // e[1][0] is the intended negative value that we care about (-1.0) as it
2056  // comes from the rotation of pi/2.
2057  EXPECT_LT(transform.e[1][0], 0.0f);
2058  auto coverage = geometry->GetCoverage(transform);
2059  ASSERT_RECT_NEAR(coverage.value(), Rect::MakeXYWH(102.5, 342.5, 85, 155));
2060 }
2061 
2062 TEST_P(EntityTest, SolidColorContentsIsOpaque) {
2063  Matrix matrix;
2064  auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2065  SolidColorContents contents(geom.get());
2066 
2067  contents.SetColor(Color::CornflowerBlue());
2068  EXPECT_TRUE(contents.IsOpaque(matrix));
2069  contents.SetColor(Color::CornflowerBlue().WithAlpha(0.5));
2070  EXPECT_FALSE(contents.IsOpaque(matrix));
2071 
2072  // Create stroked path that required alpha coverage.
2073  auto geom2 = Geometry::MakeStrokePath(
2074  flutter::DlPath::MakeLine({0, 0}, {100, 100}), {.width = 0.05});
2075  SolidColorContents contents2(geom2.get());
2076  contents2.SetColor(Color::CornflowerBlue());
2077 
2078  EXPECT_FALSE(contents2.IsOpaque(matrix));
2079 }
2080 
2081 TEST_P(EntityTest, ConicalGradientContentsIsOpaque) {
2082  Matrix matrix;
2083  auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2084  ConicalGradientContents contents(geom.get());
2085 
2086  contents.SetColors({Color::CornflowerBlue()});
2087  EXPECT_FALSE(contents.IsOpaque(matrix));
2088  contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2089  EXPECT_FALSE(contents.IsOpaque(matrix));
2090 
2091  // Create stroked path that required alpha coverage.
2092  auto geom2 = Geometry::MakeStrokePath(
2093  flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2094  {.width = 0.05f});
2095  ConicalGradientContents contents2(geom2.get());
2096  contents2.SetColors({Color::CornflowerBlue()});
2097 
2098  EXPECT_FALSE(contents2.IsOpaque(matrix));
2099 }
2100 
2101 TEST_P(EntityTest, LinearGradientContentsIsOpaque) {
2102  Matrix matrix;
2103  auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2104  LinearGradientContents contents(geom.get());
2105 
2106  contents.SetColors({Color::CornflowerBlue()});
2107  EXPECT_TRUE(contents.IsOpaque(matrix));
2108  contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2109  EXPECT_FALSE(contents.IsOpaque(matrix));
2110  contents.SetColors({Color::CornflowerBlue()});
2112  EXPECT_FALSE(contents.IsOpaque(matrix));
2113 
2114  // Create stroked path that required alpha coverage.
2115  auto geom2 = Geometry::MakeStrokePath(
2116  flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2117  {.width = 0.05f});
2118  LinearGradientContents contents2(geom2.get());
2119  contents2.SetColors({Color::CornflowerBlue()});
2120 
2121  EXPECT_FALSE(contents2.IsOpaque(matrix));
2122 }
2123 
2124 TEST_P(EntityTest, RadialGradientContentsIsOpaque) {
2125  Matrix matrix;
2126  auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2127  RadialGradientContents contents(geom.get());
2128 
2129  contents.SetColors({Color::CornflowerBlue()});
2130  EXPECT_TRUE(contents.IsOpaque(matrix));
2131  contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2132  EXPECT_FALSE(contents.IsOpaque(matrix));
2133  contents.SetColors({Color::CornflowerBlue()});
2135  EXPECT_FALSE(contents.IsOpaque(matrix));
2136 
2137  // Create stroked path that required alpha coverage.
2138  auto geom2 = Geometry::MakeStrokePath(
2139  flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2140  {.width = 0.05});
2141  RadialGradientContents contents2(geom2.get());
2142  contents2.SetColors({Color::CornflowerBlue()});
2143 
2144  EXPECT_FALSE(contents2.IsOpaque(matrix));
2145 }
2146 
2147 TEST_P(EntityTest, SweepGradientContentsIsOpaque) {
2148  Matrix matrix;
2149  auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2150  SweepGradientContents contents(geom.get());
2151 
2152  contents.SetColors({Color::CornflowerBlue()});
2153  EXPECT_TRUE(contents.IsOpaque(matrix));
2154  contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2155  EXPECT_FALSE(contents.IsOpaque(matrix));
2156  contents.SetColors({Color::CornflowerBlue()});
2158  EXPECT_FALSE(contents.IsOpaque(matrix));
2159 
2160  // Create stroked path that required alpha coverage.
2161  auto geom2 = Geometry::MakeStrokePath(
2162  flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2163  {.width = 0.05f});
2164  SweepGradientContents contents2(geom2.get());
2165  contents2.SetColors({Color::CornflowerBlue()});
2166 
2167  EXPECT_FALSE(contents2.IsOpaque(matrix));
2168 }
2169 
2170 TEST_P(EntityTest, TiledTextureContentsIsOpaque) {
2171  Matrix matrix;
2172  auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg");
2173  auto geom = Geometry::MakeCover();
2174  TiledTextureContents contents(geom.get());
2175  contents.SetTexture(bay_bridge);
2176  // This is a placeholder test. Images currently never decompress as opaque
2177  // (whether in Flutter or the playground), and so this should currently always
2178  // return false in practice.
2179  EXPECT_FALSE(contents.IsOpaque(matrix));
2180 }
2181 
2182 TEST_P(EntityTest, PointFieldGeometryCoverage) {
2183  std::vector<Point> points = {{10, 20}, {100, 200}};
2184  PointFieldGeometry geometry(points.data(), 2, 5.0, false);
2185  ASSERT_EQ(geometry.GetCoverage(Matrix()), Rect::MakeLTRB(5, 15, 105, 205));
2186  ASSERT_EQ(geometry.GetCoverage(Matrix::MakeTranslation({30, 0, 0})),
2187  Rect::MakeLTRB(35, 15, 135, 205));
2188 }
2189 
2190 TEST_P(EntityTest, ColorFilterContentsWithLargeGeometry) {
2191  Entity entity;
2192  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2193  auto src_geom = Geometry::MakeRect(Rect::MakeLTRB(-300, -500, 30000, 50000));
2194  auto src_contents = std::make_shared<SolidColorContents>(src_geom.get());
2195  src_contents->SetColor(Color::Red());
2196 
2197  auto dst_geom = Geometry::MakeRect(Rect::MakeLTRB(300, 500, 20000, 30000));
2198  auto dst_contents = std::make_shared<SolidColorContents>(dst_geom.get());
2199  dst_contents->SetColor(Color::Blue());
2200 
2201  auto contents = ColorFilterContents::MakeBlend(
2202  BlendMode::kSrcOver, {FilterInput::Make(dst_contents, false),
2203  FilterInput::Make(src_contents, false)});
2204  entity.SetContents(std::move(contents));
2205  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2206 }
2207 
2208 TEST_P(EntityTest, TextContentsCeilsGlyphScaleToDecimal) {
2209  ASSERT_EQ(TextFrame::RoundScaledFontSize(0.4321111f), Rational(43, 100));
2210  ASSERT_EQ(TextFrame::RoundScaledFontSize(0.5321111f), Rational(53, 100));
2211  ASSERT_EQ(TextFrame::RoundScaledFontSize(2.1f), Rational(21, 10));
2212  ASSERT_EQ(TextFrame::RoundScaledFontSize(0.0f), Rational(0, 1));
2213  ASSERT_EQ(TextFrame::RoundScaledFontSize(100000000.0f), Rational(48, 1));
2214 }
2215 
2216 TEST_P(EntityTest, SpecializationConstantsAreAppliedToVariants) {
2217  ContentContext& content_context = GetContentContext();
2218 
2219  auto default_gyph = content_context.GetGlyphAtlasPipeline({
2220  .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2221  .has_depth_stencil_attachments = false,
2222  });
2223  auto alt_gyph = content_context.GetGlyphAtlasPipeline(
2224  {.color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2225  .has_depth_stencil_attachments = true});
2226 
2227  EXPECT_NE(default_gyph, alt_gyph);
2228  EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2229  alt_gyph->GetDescriptor().GetSpecializationConstants());
2230 
2231  auto use_a8 = GetContext()->GetCapabilities()->GetDefaultGlyphAtlasFormat() ==
2233 
2234  std::vector<Scalar> expected_constants = {static_cast<Scalar>(use_a8)};
2235  EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2236  expected_constants);
2237 }
2238 
2239 TEST_P(EntityTest, DecalSpecializationAppliedToMorphologyFilter) {
2240  ContentContext& content_context = GetContentContext();
2241  auto default_color_burn = content_context.GetMorphologyFilterPipeline({
2242  .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2243  });
2244 
2245  auto decal_supported = static_cast<Scalar>(
2246  GetContext()->GetCapabilities()->SupportsDecalSamplerAddressMode());
2247  std::vector<Scalar> expected_constants = {decal_supported};
2248  ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(),
2249  expected_constants);
2250 }
2251 
2252 // This doesn't really tell you if the hashes will have frequent
2253 // collisions, but since this type is only used to hash a bounded
2254 // set of options, we can just compare benchmarks.
2255 TEST_P(EntityTest, ContentContextOptionsHasReasonableHashFunctions) {
2256  ContentContextOptions opts;
2257  auto hash_a = opts.ToKey();
2258 
2260  auto hash_b = opts.ToKey();
2261 
2262  opts.has_depth_stencil_attachments = false;
2263  auto hash_c = opts.ToKey();
2264 
2266  auto hash_d = opts.ToKey();
2267 
2268  EXPECT_NE(hash_a, hash_b);
2269  EXPECT_NE(hash_b, hash_c);
2270  EXPECT_NE(hash_c, hash_d);
2271 }
2272 
2273 #ifdef FML_OS_LINUX
2274 TEST_P(EntityTest, FramebufferFetchVulkanBindingOffsetIsTheSame) {
2275  // Using framebuffer fetch on Vulkan requires that we maintain a subpass input
2276  // binding that we don't have a good route for configuring with the
2277  // current metadata approach. This test verifies that the binding value
2278  // doesn't change
2279  // from the expected constant.
2280  // See also:
2281  // * impeller/renderer/backend/vulkan/binding_helpers_vk.cc
2282  // * impeller/entity/shaders/blending/framebuffer_blend.frag
2283  // This test only works on Linux because macOS hosts incorrectly
2284  // populate the
2285  // Vulkan descriptor sets based on the MSL compiler settings.
2286 
2287  bool expected_layout = false;
2289  FragmentShader::kDescriptorSetLayouts) {
2290  if (layout.binding == 64 &&
2291  layout.descriptor_type == DescriptorType::kInputAttachment) {
2292  expected_layout = true;
2293  }
2294  }
2295  EXPECT_TRUE(expected_layout);
2296 }
2297 #endif
2298 
2299 TEST_P(EntityTest, FillPathGeometryGetPositionBufferReturnsExpectedMode) {
2300  RenderTarget target;
2301  testing::MockRenderPass mock_pass(GetContext(), target);
2302 
2303  auto get_result = [this, &mock_pass](const flutter::DlPath& path) {
2304  auto geometry = Geometry::MakeFillPath(
2305  path, /* inner rect */ Rect::MakeLTRB(0, 0, 100, 100));
2306  return geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2307  };
2308 
2309  // Convex path
2310  {
2311  GeometryResult result =
2312  get_result(flutter::DlPath::MakeRect(Rect::MakeLTRB(0, 0, 100, 100)));
2313  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal);
2314  }
2315 
2316  // Concave path
2317  {
2318  flutter::DlPath path = flutter::DlPathBuilder{}
2319  .MoveTo({0, 0})
2320  .LineTo({100, 0})
2321  .LineTo({100, 100})
2322  .LineTo({51, 50})
2323  .Close()
2324  .TakePath();
2325  GeometryResult result = get_result(path);
2326  EXPECT_EQ(result.mode, GeometryResult::Mode::kNonZero);
2327  }
2328 }
2329 
2330 TEST_P(EntityTest, StrokeArcGeometryGetPositionBufferReturnsExpectedMode) {
2331  RenderTarget target;
2332  testing::MockRenderPass mock_pass(GetContext(), target);
2333  Rect oval_bounds = Rect::MakeLTRB(100, 100, 200, 200);
2334 
2335  // Butt caps never overlap
2336  {
2337  StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kButt};
2338  for (auto start = 0; start < 360; start += 60) {
2339  for (auto sweep = 0; sweep < 360; sweep += 12) {
2340  auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2341  Degrees(sweep), stroke);
2342 
2343  GeometryResult result =
2344  geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2345 
2346  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2347  << "start: " << start << " sweep: " << sweep;
2348  }
2349  }
2350  }
2351 
2352  // Round caps with 10 stroke width overlap starting at 348.6 degrees
2353  {
2354  StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kRound};
2355  for (auto start = 0; start < 360; start += 60) {
2356  for (auto sweep = 0; sweep < 360; sweep += 12) {
2357  auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2358  Degrees(sweep), stroke);
2359 
2360  GeometryResult result =
2361  geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2362 
2363  if (sweep < 348.6) {
2364  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2365  << "start: " << start << " sweep: " << sweep;
2366  } else {
2367  EXPECT_EQ(result.mode, GeometryResult::Mode::kPreventOverdraw)
2368  << "start: " << start << " sweep: " << sweep;
2369  }
2370  }
2371  }
2372  }
2373 
2374  // Round caps with 50 stroke width overlap starting at 300.1 degrees
2375  {
2376  StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kRound};
2377  for (auto start = 0; start < 360; start += 60) {
2378  for (auto sweep = 0; sweep < 360; sweep += 12) {
2379  auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2380  Degrees(sweep), stroke);
2381 
2382  GeometryResult result =
2383  geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2384 
2385  if (sweep < 300.0) {
2386  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2387  << "start: " << start << " sweep: " << sweep;
2388  } else {
2389  EXPECT_EQ(result.mode, GeometryResult::Mode::kPreventOverdraw)
2390  << "start: " << start << " sweep: " << sweep;
2391  }
2392  }
2393  }
2394  }
2395 
2396  // Square caps with 10 stroke width overlap starting at 347.4 degrees
2397  {
2398  StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kSquare};
2399  for (auto start = 0; start < 360; start += 60) {
2400  for (auto sweep = 0; sweep < 360; sweep += 12) {
2401  auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2402  Degrees(sweep), stroke);
2403 
2404  GeometryResult result =
2405  geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2406 
2407  if (sweep < 347.4) {
2408  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2409  << "start: " << start << " sweep: " << sweep;
2410  } else {
2411  EXPECT_EQ(result.mode, GeometryResult::Mode::kPreventOverdraw)
2412  << "start: " << start << " sweep: " << sweep;
2413  }
2414  }
2415  }
2416  }
2417 
2418  // Square caps with 50 stroke width overlap starting at 270.1 degrees
2419  {
2420  StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kSquare};
2421  for (auto start = 0; start < 360; start += 60) {
2422  for (auto sweep = 0; sweep < 360; sweep += 12) {
2423  auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2424  Degrees(sweep), stroke);
2425 
2426  GeometryResult result =
2427  geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2428 
2429  if (sweep < 270.1) {
2430  EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2431  << "start: " << start << " sweep: " << sweep;
2432  } else {
2433  EXPECT_EQ(result.mode, GeometryResult::Mode::kPreventOverdraw)
2434  << "start: " << start << " sweep: " << sweep;
2435  }
2436  }
2437  }
2438  }
2439 }
2440 
2441 TEST_P(EntityTest, FailOnValidationError) {
2442  if (GetParam() != PlaygroundBackend::kVulkan) {
2443  GTEST_SKIP() << "Validation is only fatal on Vulkan backend.";
2444  }
2445  EXPECT_DEATH(
2446  // The easiest way to trigger a validation error is to try to compile
2447  // a shader with an unsupported pixel format.
2448  GetContentContext().GetBlendColorBurnPipeline({
2449  .color_attachment_pixel_format = PixelFormat::kUnknown,
2450  .has_depth_stencil_attachments = false,
2451  }),
2452  "");
2453 }
2454 
2455 TEST_P(EntityTest, CanComputeGeometryForEmptyPathsWithoutCrashing) {
2456  flutter::DlPath path = flutter::DlPath::MakeRect(Rect::MakeLTRB(0, 0, 0, 0));
2457 
2458  EXPECT_TRUE(path.GetBounds().IsEmpty());
2459 
2460  auto geom = Geometry::MakeFillPath(path);
2461 
2462  Entity entity;
2463  RenderTarget target =
2464  GetContentContext().GetRenderTargetCache()->CreateOffscreen(*GetContext(),
2465  {1, 1}, 1u);
2466  testing::MockRenderPass render_pass(GetContext(), target);
2467  auto position_result =
2468  geom->GetPositionBuffer(GetContentContext(), entity, render_pass);
2469 
2470  EXPECT_EQ(position_result.vertex_buffer.vertex_count, 0u);
2471 
2472  EXPECT_EQ(geom->GetResultMode(), GeometryResult::Mode::kNormal);
2473 }
2474 
2475 TEST_P(EntityTest, CanRenderEmptyPathsWithoutCrashing) {
2476  flutter::DlPath path = flutter::DlPath::MakeRect(Rect::MakeLTRB(0, 0, 0, 0));
2477 
2478  EXPECT_TRUE(path.GetBounds().IsEmpty());
2479 
2480  std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
2481  auto contents = std::make_shared<SolidColorContents>(geom.get());
2482  contents->SetColor(Color::Red());
2483 
2484  Entity entity;
2485  entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2486  entity.SetContents(contents);
2487 
2488  ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2489 }
2490 
2491 TEST_P(EntityTest, DrawSuperEllipse) {
2492  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2493  // UI state.
2494  static float alpha = 10;
2495  static float beta = 10;
2496  static float radius = 40;
2497  static int degree = 4;
2498  static Color color = Color::Red();
2499 
2500  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2501  ImGui::SliderFloat("Alpha", &alpha, 0, 100);
2502  ImGui::SliderFloat("Beta", &beta, 0, 100);
2503  ImGui::SliderInt("Degreee", &degree, 1, 20);
2504  ImGui::SliderFloat("Radius", &radius, 0, 400);
2505  ImGui::ColorEdit4("Color", reinterpret_cast<float*>(&color));
2506  ImGui::End();
2507 
2508  std::unique_ptr<SuperellipseGeometry> geom =
2509  std::make_unique<SuperellipseGeometry>(Point{400, 400}, radius, degree,
2510  alpha, beta);
2511  auto contents = std::make_shared<SolidColorContents>(geom.get());
2512  contents->SetColor(color);
2513 
2514  Entity entity;
2515  entity.SetContents(contents);
2516 
2517  return entity.Render(context, pass);
2518  };
2519 
2520  ASSERT_TRUE(OpenPlaygroundHere(callback));
2521 }
2522 
2523 TEST_P(EntityTest, DrawRoundSuperEllipse) {
2524  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2525  // UI state.
2526  static int style_index = 0;
2527  static Point center = {830, 830};
2528  static Point size = {600, 600};
2529  static bool horizontal_symmetry = true;
2530  static bool vertical_symmetry = true;
2531  static bool corner_symmetry = true;
2532 
2533  const char* style_options[] = {"Fill", "Stroke"};
2534 
2535  // Initially radius_tl[0] will be mirrored to all 8 values since all 3
2536  // symmetries are enabled.
2537  static std::array<float, 2> radius_tl = {200};
2538  static std::array<float, 2> radius_tr;
2539  static std::array<float, 2> radius_bl;
2540  static std::array<float, 2> radius_br;
2541 
2542  auto AddRadiusControl = [](std::array<float, 2>& radii, const char* tb_name,
2543  const char* lr_name) {
2544  std::string name = "Radius";
2545  if (!horizontal_symmetry || !vertical_symmetry) {
2546  name += ":";
2547  }
2548  if (!vertical_symmetry) {
2549  name = name + " " + tb_name;
2550  }
2551  if (!horizontal_symmetry) {
2552  name = name + " " + lr_name;
2553  }
2554  if (corner_symmetry) {
2555  ImGui::SliderFloat(name.c_str(), radii.data(), 0, 1000);
2556  } else {
2557  ImGui::SliderFloat2(name.c_str(), radii.data(), 0, 1000);
2558  }
2559  };
2560 
2561  if (corner_symmetry) {
2562  radius_tl[1] = radius_tl[0];
2563  radius_tr[1] = radius_tr[0];
2564  radius_bl[1] = radius_bl[0];
2565  radius_br[1] = radius_br[0];
2566  }
2567 
2568  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2569  {
2570  ImGui::Combo("Style", &style_index, style_options,
2571  sizeof(style_options) / sizeof(char*));
2572  ImGui::SliderFloat2("Center", &center.x, 0, 1000);
2573  ImGui::SliderFloat2("Size", &size.x, 0, 1000);
2574  ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry);
2575  ImGui::Checkbox("Symmetry: Vertical", &vertical_symmetry);
2576  ImGui::Checkbox("Symmetry: Corners", &corner_symmetry);
2577  AddRadiusControl(radius_tl, "Top", "Left");
2578  if (!horizontal_symmetry) {
2579  AddRadiusControl(radius_tr, "Top", "Right");
2580  } else {
2581  radius_tr = radius_tl;
2582  }
2583  if (!vertical_symmetry) {
2584  AddRadiusControl(radius_bl, "Bottom", "Left");
2585  } else {
2586  radius_bl = radius_tl;
2587  }
2588  if (!horizontal_symmetry && !vertical_symmetry) {
2589  AddRadiusControl(radius_br, "Bottom", "Right");
2590  } else {
2591  if (horizontal_symmetry) {
2592  radius_br = radius_bl;
2593  } else {
2594  radius_br = radius_tr;
2595  }
2596  }
2597  }
2598 
2599  ImGui::End();
2600 
2601  RoundingRadii radii{
2602  .top_left = {radius_tl[0], radius_tl[1]},
2603  .top_right = {radius_tr[0], radius_tr[1]},
2604  .bottom_left = {radius_bl[0], radius_bl[1]},
2605  .bottom_right = {radius_br[0], radius_br[1]},
2606  };
2607 
2609  Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2610 
2611  flutter::DlPath path;
2612  std::unique_ptr<Geometry> geom;
2613  if (style_index == 0) {
2614  geom = std::make_unique<RoundSuperellipseGeometry>(
2615  Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2616  } else {
2617  path = flutter::DlPath::MakeRoundSuperellipse(rse);
2618  geom = Geometry::MakeStrokePath(path, {.width = 2.0f});
2619  }
2620 
2621  auto contents = std::make_shared<SolidColorContents>(geom.get());
2622  contents->SetColor(Color::Red());
2623 
2624  Entity entity;
2625  entity.SetContents(contents);
2626 
2627  return entity.Render(context, pass);
2628  };
2629 
2630  ASSERT_TRUE(OpenPlaygroundHere(callback));
2631 }
2632 
2633 TEST_P(EntityTest, DrawRoundSuperEllipseWithLargeN) {
2634  // This playground shows the enlarged corner of a rounded superellipse to
2635  // compare pathing algorithm against the filling algorithm (benchmark) at very
2636  // large ratio.
2637  auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2638  constexpr float corner_radius = 100.0;
2639 
2640  // UI state.
2641  static float logarithm_of_ratio = 1.5; // ratio = size / corner_radius
2642 
2643  float ratio = std::exp(logarithm_of_ratio);
2644 
2645  float rect_size = corner_radius * ratio;
2646  Rect rect = Rect::MakeLTRB(0, 0, rect_size, rect_size);
2647  constexpr float screen_canvas_padding = 200.0f;
2648  constexpr float screen_canvas_size = 1000.0f;
2649 
2650  // Scale so that "corner radius" is as long as half the canvas.
2651  float scale = screen_canvas_size / 2 / corner_radius;
2652 
2653  ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2654  {
2655  ImGui::SliderFloat("log(Ratio)", &logarithm_of_ratio, 1.0, 8.0);
2656  ImGui::LabelText("Ratio", "%.2g", static_cast<double>(ratio));
2657  ImGui::Text(" where Ratio = RectSize / CornerRadius");
2658  }
2659  ImGui::End();
2660 
2661  auto top_right = Vector2(screen_canvas_size * 1.3f, screen_canvas_padding);
2662  auto transform = Matrix::MakeTranslation(top_right) *
2663  Matrix::MakeScale(Vector2(scale, scale)) *
2664  Matrix::MakeTranslation(Vector2(-rect_size, 0));
2665  bool success = true;
2666 
2667  auto fill_geom =
2668  std::make_unique<RoundSuperellipseGeometry>(rect, corner_radius);
2669  // Fill
2670  {
2671  auto contents = std::make_shared<SolidColorContents>(fill_geom.get());
2672  contents->SetColor(Color::Red());
2673 
2674  Entity entity;
2675  entity.SetContents(contents);
2676  entity.SetTransform(transform);
2677 
2678  success = success && entity.Render(context, pass);
2679  }
2680 
2681  // Stroke
2682  auto path = flutter::DlPath::MakeRoundSuperellipse(
2683  RoundSuperellipse::MakeRectRadius(rect, corner_radius));
2684  auto stroke_geom = Geometry::MakeStrokePath(path, {.width = 2 / scale});
2685  {
2686  auto contents = std::make_shared<SolidColorContents>(stroke_geom.get());
2687  contents->SetColor(Color::Blue());
2688 
2689  Entity entity;
2690  entity.SetContents(contents);
2691  entity.SetTransform(transform);
2692 
2693  success = success && entity.Render(context, pass);
2694  }
2695 
2696  // Draw a ruler to show the length in portion of rect size.
2697  auto screen_top_right =
2698  Matrix::MakeScale(GetContentScale()).Invert() * top_right;
2699  constexpr float font_size = 13.0f;
2700  for (int i = -1; i < 100; i++) {
2701  float screen_offset_y = static_cast<float>(i) * 20.0f;
2702  std::string label;
2703  if (i == -1) {
2704  label = "Ruler: (in portion of rect size)";
2705  } else if (i == 0) {
2706  label = "- 0.0";
2707  } else {
2708  float portion_of_rect =
2709  screen_offset_y * GetContentScale().y / scale / rect_size;
2710  label = std::format("- {:.2g}", portion_of_rect);
2711  }
2712  ImGui::GetBackgroundDrawList()->AddText(
2713  nullptr, font_size,
2714  // Draw the ruler at around the flat part of the curve, which is
2715  // somewhere to the left of the top right corner.
2716  //
2717  // Offset vertically by font_size/2 so that the hyphen aligns with the
2718  // top of shape.
2719  {screen_top_right.x - 500,
2720  screen_top_right.y + screen_offset_y - font_size / 2},
2721  IM_COL32_WHITE, label.c_str());
2722  }
2723  return success;
2724  };
2725 
2726  ASSERT_TRUE(OpenPlaygroundHere(callback));
2727 }
2728 
2729 TEST_P(EntityTest, CanDrawRoundSuperEllipseWithTinyRadius) {
2730  // Regression test for https://github.com/flutter/flutter/issues/176894
2731  // Verify that a radius marginally below the minimum threshold can be
2732  // processed safely. The expectation is that the rounded corners degenerate
2733  // into sharp corners (four corner points) and that no NaNs or crashes occur.
2734  auto geom = Geometry::MakeRoundSuperellipse(
2735  Rect::MakeLTRB(200, 200, 300, 300), 0.5 * kEhCloseEnough);
2736 
2737  ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2738  Entity entity;
2739 
2740  auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2741 
2742  RenderTargetAllocator allocator(
2743  content_context.GetContext()->GetResourceAllocator());
2744 
2745  auto render_target = allocator.CreateOffscreen(
2746  *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2747  auto pass = cmd_buffer->CreateRenderPass(render_target);
2748 
2749  GeometryResult result =
2750  geom->GetPositionBuffer(content_context, entity, *pass);
2751 
2752  EXPECT_EQ(result.vertex_buffer.vertex_count, 4u);
2753  Point* written_data = reinterpret_cast<Point*>(
2756 
2757  std::vector<Point> expected = {Point(300.0, 200.0), Point(300.0, 300.0),
2758  Point(200.0, 200.0), Point(200.0, 300.0)};
2759 
2760  for (size_t i = 0; i < expected.size(); i++) {
2761  const Point& point = written_data[i];
2762  EXPECT_NEAR(point.x, expected[i].x, 0.1);
2763  EXPECT_NEAR(point.y, expected[i].y, 0.1);
2764  }
2765 }
2766 
2767 TEST_P(EntityTest, CanDrawRoundSuperEllipseWithJustEnoughRadius) {
2768  // Regression test for https://github.com/flutter/flutter/issues/176894
2769  // Verify that a radius marginally above the minimum threshold can be
2770  // processed safely. The expectation is that the rounded corners are
2771  // drawn as rounded and that no NaNs or crashes occur.
2772  auto geom = Geometry::MakeRoundSuperellipse(
2773  Rect::MakeLTRB(200, 200, 300, 300), 1.1 * kEhCloseEnough);
2774 
2775  ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2776  Entity entity;
2777 
2778  auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2779 
2780  RenderTargetAllocator allocator(
2781  content_context.GetContext()->GetResourceAllocator());
2782 
2783  auto render_target = allocator.CreateOffscreen(
2784  *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2785  auto pass = cmd_buffer->CreateRenderPass(render_target);
2786 
2787  GeometryResult result =
2788  geom->GetPositionBuffer(content_context, entity, *pass);
2789 
2790  EXPECT_EQ(result.vertex_buffer.vertex_count, 200u);
2791  Point* written_data = reinterpret_cast<Point*>(
2794 
2795  std::vector<Point> expected_head = {Point(250.0, 200.0), Point(299.9, 200.0),
2796  Point(200.1, 200.0), Point(299.9, 200.0)};
2797 
2798  for (size_t i = 0; i < expected_head.size(); i++) {
2799  const Point& point = written_data[i];
2800  EXPECT_NEAR(point.x, expected_head[i].x, 0.1);
2801  EXPECT_NEAR(point.y, expected_head[i].y, 0.1);
2802  }
2803 }
2804 
2805 TEST_P(EntityTest, SolidColorApplyColorFilter) {
2806  auto geom = Geometry::MakeCover();
2807  auto contents = SolidColorContents(geom.get());
2808  contents.SetColor(Color::CornflowerBlue().WithAlpha(0.75));
2809  auto result = contents.ApplyColorFilter([](const Color& color) {
2810  return color.Blend(Color::LimeGreen().WithAlpha(0.75), BlendMode::kScreen);
2811  });
2812  ASSERT_TRUE(result);
2813  ASSERT_COLOR_NEAR(contents.GetColor(),
2814  Color(0.424452, 0.828743, 0.79105, 0.9375));
2815 }
2816 
2817 #define APPLY_COLOR_FILTER_GRADIENT_TEST(name) \
2818  TEST_P(EntityTest, name##GradientApplyColorFilter) { \
2819  auto geom = Geometry::MakeCover(); \
2820  auto contents = name##GradientContents(geom.get()); \
2821  contents.SetColors({Color::CornflowerBlue().WithAlpha(0.75)}); \
2822  auto result = contents.ApplyColorFilter([](const Color& color) { \
2823  return color.Blend(Color::LimeGreen().WithAlpha(0.75), \
2824  BlendMode::kScreen); \
2825  }); \
2826  ASSERT_TRUE(result); \
2827  \
2828  std::vector<Color> expected = {Color(0.433247, 0.879523, 0.825324, 0.75)}; \
2829  ASSERT_COLORS_NEAR(contents.GetColors(), expected); \
2830  }
2831 
2836 
2837 TEST_P(EntityTest, GiantStrokePathAllocation) {
2838  flutter::DlPathBuilder builder;
2839  for (int i = 0; i < 10000; i++) {
2840  builder.LineTo(Point(i, i));
2841  }
2842  flutter::DlPath path = builder.TakePath();
2843  auto geom = Geometry::MakeStrokePath(path, {.width = 10.0f});
2844 
2845  ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2846  Entity entity;
2847 
2848  auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2849 
2850  RenderTargetAllocator allocator(
2851  content_context.GetContext()->GetResourceAllocator());
2852 
2853  auto render_target = allocator.CreateOffscreen(
2854  *content_context.GetContext(), /*size=*/{10, 10}, /*mip_count=*/1);
2855  auto pass = cmd_buffer->CreateRenderPass(render_target);
2856 
2857  GeometryResult result =
2858  geom->GetPositionBuffer(content_context, entity, *pass);
2859 
2860  // Validate the buffer data overflowed the small buffer
2861  EXPECT_GT(result.vertex_buffer.vertex_count, kPointArenaSize);
2862 
2863  // Validate that there are no uninitialized points near the gap.
2864  Point* written_data = reinterpret_cast<Point*>(
2867 
2868  std::vector<Point> expected = {
2869  Point(2043.46, 2050.54), //
2870  Point(2050.54, 2043.46), //
2871  Point(2044.46, 2051.54), //
2872  Point(2051.54, 2044.46), //
2873  Point(2045.46, 2052.54) //
2874  };
2875 
2876  Point point = written_data[kPointArenaSize - 2];
2877  EXPECT_NEAR(point.x, expected[0].x, 0.1);
2878  EXPECT_NEAR(point.y, expected[0].y, 0.1);
2879 
2880  point = written_data[kPointArenaSize - 1];
2881  EXPECT_NEAR(point.x, expected[1].x, 0.1);
2882  EXPECT_NEAR(point.y, expected[1].y, 0.1);
2883 
2884  point = written_data[kPointArenaSize];
2885  EXPECT_NEAR(point.x, expected[2].x, 0.1);
2886  EXPECT_NEAR(point.y, expected[2].y, 0.1);
2887 
2888  point = written_data[kPointArenaSize + 1];
2889  EXPECT_NEAR(point.x, expected[3].x, 0.1);
2890  EXPECT_NEAR(point.y, expected[3].y, 0.1);
2891 
2892  point = written_data[kPointArenaSize + 2];
2893  EXPECT_NEAR(point.x, expected[4].x, 0.1);
2894  EXPECT_NEAR(point.y, expected[4].y, 0.1);
2895 }
2896 
2898  public:
2900  : DeviceBuffer(desc), storage_(desc.size) {}
2901 
2902  bool SetLabel(std::string_view label) override { return true; }
2903  bool SetLabel(std::string_view label, Range range) override { return true; }
2904  bool OnCopyHostBuffer(const uint8_t* source,
2905  Range source_range,
2906  size_t offset) {
2907  return true;
2908  }
2909 
2910  uint8_t* OnGetContents() const override {
2911  return const_cast<uint8_t*>(storage_.data());
2912  }
2913 
2914  void Flush(std::optional<Range> range) const override {
2915  flush_called_ = true;
2916  }
2917 
2918  bool flush_called() const { return flush_called_; }
2919 
2920  private:
2921  std::vector<uint8_t> storage_;
2922  mutable bool flush_called_ = false;
2923 };
2924 
2926  public:
2928  return ISize(1024, 1024);
2929  };
2930 
2931  std::shared_ptr<DeviceBuffer> OnCreateBuffer(
2932  const DeviceBufferDescriptor& desc) override {
2933  return std::make_shared<FlushTestDeviceBuffer>(desc);
2934  };
2935 
2936  std::shared_ptr<Texture> OnCreateTexture(const TextureDescriptor& desc,
2937  bool threadsafe) override {
2938  return nullptr;
2939  }
2940 };
2941 
2943  public:
2945  const std::shared_ptr<Context>& context,
2946  const std::shared_ptr<TypographerContext>& typographer_context,
2947  const std::shared_ptr<Allocator>& allocator)
2948  : ContentContext(context, typographer_context) {
2950  allocator, context->GetIdleWaiter(),
2951  context->GetCapabilities()->GetMinimumUniformAlignment()));
2953  allocator, context->GetIdleWaiter(),
2954  context->GetCapabilities()->GetMinimumUniformAlignment()));
2955  }
2956 };
2957 
2958 TEST_P(EntityTest, RoundSuperellipseGetPositionBufferFlushes) {
2959  RenderTarget target;
2960  testing::MockRenderPass mock_pass(GetContext(), target);
2961 
2962  auto content_context = std::make_shared<FlushTestContentContext>(
2963  GetContext(), GetTypographerContext(),
2964  std::make_shared<FlushTestAllocator>());
2965  auto geometry =
2966  Geometry::MakeRoundSuperellipse(Rect::MakeLTRB(0, 0, 100, 100), 5);
2967  auto result = geometry->GetPositionBuffer(*content_context, {}, mock_pass);
2968 
2969  auto device_buffer = reinterpret_cast<const FlushTestDeviceBuffer*>(
2970  result.vertex_buffer.vertex_buffer.GetBuffer());
2971  EXPECT_TRUE(device_buffer->flush_called());
2972 }
2973 
2974 } // namespace testing
2975 } // namespace impeller
2976 
2977 // NOLINTEND(bugprone-unchecked-optional-access)
An object that allocates device memory.
Definition: allocator.h:24
static std::shared_ptr< ColorFilterContents > MakeColorMatrix(FilterInput::Ref input, const ColorMatrix &color_matrix)
static std::shared_ptr< ColorFilterContents > MakeSrgbToLinearFilter(FilterInput::Ref input)
static std::shared_ptr< ColorFilterContents > MakeLinearToSrgbFilter(FilterInput::Ref input)
static std::shared_ptr< ColorFilterContents > MakeBlend(BlendMode blend_mode, FilterInput::Vector inputs, std::optional< Color > foreground_color=std::nullopt)
the [inputs] are expected to be in the order of dst, src.
void SetColors(std::vector< Color > colors)
void SetTransientsDataBuffer(std::shared_ptr< HostBuffer > host_buffer)
PipelineRef GetMorphologyFilterPipeline(ContentContextOptions opts) const
PipelineRef GetGlyphAtlasPipeline(ContentContextOptions opts) const
const std::shared_ptr< RenderTargetAllocator > & GetRenderTargetCache() const
void SetTransientsIndexesBuffer(std::shared_ptr< HostBuffer > host_buffer)
std::shared_ptr< Context > GetContext() const
virtual bool IsOpaque(const Matrix &transform) const
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
Definition: contents.cc:52
To do anything rendering related with Impeller, you need a context.
Definition: context.h:69
static BufferView AsBufferView(std::shared_ptr< DeviceBuffer > buffer)
Create a buffer view of this entire buffer.
virtual uint8_t * OnGetContents() const =0
void SetTransform(const Matrix &transform)
Set the global transform matrix for this Entity.
Definition: entity.cc:62
std::optional< Rect > GetCoverage() const
Definition: entity.cc:66
BlendMode GetBlendMode() const
Definition: entity.cc:102
void SetContents(std::shared_ptr< Contents > contents)
Definition: entity.cc:74
void SetBlendMode(BlendMode blend_mode)
Definition: entity.cc:98
bool Render(const ContentContext &renderer, RenderPass &parent_pass) const
Definition: entity.cc:145
const Matrix & GetTransform() const
Get the global transform matrix for this Entity.
Definition: entity.cc:46
static constexpr BlendMode kLastPipelineBlendMode
Definition: entity.h:28
static std::shared_ptr< FilterContents > MakeGaussianBlur(const FilterInput::Ref &input, Sigma sigma_x, Sigma sigma_y, Entity::TileMode tile_mode=Entity::TileMode::kDecal, std::optional< Rect > bounds=std::nullopt, BlurStyle mask_blur_style=BlurStyle::kNormal, const Geometry *mask_geometry=nullptr)
@ kNormal
Blurred inside and outside.
@ kOuter
Nothing inside, blurred outside.
@ kInner
Blurred inside, nothing outside.
@ kSolid
Solid inside, blurred outside.
static std::shared_ptr< FilterContents > MakeMorphology(FilterInput::Ref input, Radius radius_x, Radius radius_y, MorphType morph_type)
static std::shared_ptr< FilterContents > MakeBorderMaskBlur(FilterInput::Ref input, Sigma sigma_x, Sigma sigma_y, BlurStyle blur_style=BlurStyle::kNormal)
static std::shared_ptr< FilterContents > MakeYUVToRGBFilter(std::shared_ptr< Texture > y_texture, std::shared_ptr< Texture > uv_texture, YUVColorSpace yuv_color_space)
static FilterInput::Ref Make(Variant input, bool msaa_enabled=true)
Definition: filter_input.cc:19
static std::unique_ptr< Geometry > MakeFillPath(const flutter::DlPath &path, std::optional< Rect > inner_rect=std::nullopt)
Definition: geometry.cc:62
static std::unique_ptr< Geometry > MakeRect(const Rect &rect)
Definition: geometry.cc:83
static std::unique_ptr< Geometry > MakeStrokePath(const flutter::DlPath &path, const StrokeParameters &stroke={})
Definition: geometry.cc:68
static std::unique_ptr< Geometry > MakeRoundSuperellipse(const Rect &rect, Scalar corner_radius)
Definition: geometry.cc:130
static std::unique_ptr< Geometry > MakeCover()
Definition: geometry.cc:79
static std::unique_ptr< Geometry > MakeStrokedArc(const Rect &oval_bounds, Degrees start, Degrees sweep, const StrokeParameters &stroke)
Definition: geometry.cc:116
static std::shared_ptr< HostBuffer > Create(const std::shared_ptr< Allocator > &allocator, const std::shared_ptr< const IdleWaiter > &idle_waiter, size_t minimum_uniform_alignment)
Definition: host_buffer.cc:21
void SetTileMode(Entity::TileMode tile_mode)
void SetColors(std::vector< Color > colors)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
A geometry class specialized for Canvas::DrawPoints.
std::optional< Rect > GetCoverage(const Matrix &transform) const override
The coverage rectangle of this geometry, transformed by the transform argument.
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetTileMode(Entity::TileMode tile_mode)
void SetColors(std::vector< Color > colors)
Render passes encode render commands directed as one specific render target into an underlying comman...
Definition: render_pass.h:30
FragmentShader_ FragmentShader
Definition: pipeline.h:169
a wrapper around the impeller [Allocator] instance that can be used to provide caching of allocated r...
virtual RenderTarget CreateOffscreen(const Context &context, ISize size, int mip_count, std::string_view label="Offscreen", RenderTarget::AttachmentConfig color_attachment_config=RenderTarget::kDefaultColorAttachmentConfig, std::optional< RenderTarget::AttachmentConfig > stencil_attachment_config=RenderTarget::kDefaultStencilAttachmentConfig, const std::shared_ptr< Texture > &existing_color_texture=nullptr, const std::shared_ptr< Texture > &existing_depth_stencil_texture=nullptr, std::optional< PixelFormat > target_pixel_format=std::nullopt)
static BufferView EmplaceUniform(const uint8_t *source_data, HostBuffer &host_buffer, const RuntimeUniformDescription &uniform)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
A Geometry that produces fillable vertices representing the stroked outline of a |DlPath| object usin...
void SetTileMode(Entity::TileMode tile_mode)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetColors(std::vector< Color > colors)
static Rational RoundScaledFontSize(Scalar scale)
Definition: text_frame.cc:55
static std::shared_ptr< TextureContents > MakeRect(Rect destination)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetTexture(std::shared_ptr< Texture > texture)
static std::unique_ptr< UberSDFContents > Make(const UberSDFParameters &params, std::unique_ptr< Geometry > geometry)
VertexBuffer CreateVertexBuffer(HostBuffer &data_host_buffer, HostBuffer &indexes_host_buffer) const
VertexBufferBuilder & AddVertices(std::initializer_list< VertexType_ > vertices)
std::shared_ptr< Texture > OnCreateTexture(const TextureDescriptor &desc, bool threadsafe) override
std::shared_ptr< DeviceBuffer > OnCreateBuffer(const DeviceBufferDescriptor &desc) override
ISize GetMaxTextureSizeSupported() const override
FlushTestContentContext(const std::shared_ptr< Context > &context, const std::shared_ptr< TypographerContext > &typographer_context, const std::shared_ptr< Allocator > &allocator)
bool OnCopyHostBuffer(const uint8_t *source, Range source_range, size_t offset)
void Flush(std::optional< Range > range) const override
bool SetLabel(std::string_view label, Range range) override
bool SetLabel(std::string_view label) override
FlushTestDeviceBuffer(const DeviceBufferDescriptor &desc)
Vector2 blur_radius
Blur radius in source pixels based on scaled_sigma.
Vector2 padding
The halo padding in source space.
#define ASSERT_RECT_NEAR(a, b)
#define ASSERT_COLOR_NEAR(a, b)
static std::optional< RuntimeStageBackend > GetRuntimeStageBackend(TargetPlatform target_platform)
Definition: reflector.cc:347
TEST(AllocationSizeTest, CanCreateTypedAllocations)
TEST_P(AiksTest, DrawAtlasNoColor)
APPLY_COLOR_FILTER_GRADIENT_TEST(Linear)
INSTANTIATE_PLAYGROUND_SUITE(AiksTest)
static Vector3 RGBToYUV(Vector3 rgb, YUVColorSpace yuv_color_space)
static std::vector< std::shared_ptr< Texture > > CreateTestYUVTextures(Context *context, YUVColorSpace yuv_color_space)
YUVColorSpace
Definition: color.h:54
Join
An enum that describes ways to join two segments of a path.
@ kPoint
Draws a point at each input vertex.
Point Vector2
Definition: point.h:430
constexpr float kPi
Definition: constants.h:26
float Scalar
Definition: scalar.h:19
Point DrawPlaygroundPoint(PlaygroundPoint &point)
Definition: widgets.cc:11
std::tuple< Point, Point > DrawPlaygroundLine(PlaygroundPoint &point_a, PlaygroundPoint &point_b)
Definition: widgets.cc:51
constexpr float kEhCloseEnough
Definition: constants.h:57
TPoint< Scalar > Point
Definition: point.h:426
Cap
An enum that describes ways to decorate the end of a path contour.
LinePipeline::FragmentShader FS
constexpr float kPiOver2
Definition: constants.h:32
flutter::DlPath DlPath
Definition: dl_dispatcher.h:34
BlendMode
Definition: color.h:58
LinePipeline::VertexShader VS
void MoveTo(PathBuilder *builder, Scalar x, Scalar y)
Definition: tessellator.cc:20
static constexpr size_t kPointArenaSize
The size of the point arena buffer stored on the tessellator.
Definition: tessellator.h:25
void LineTo(PathBuilder *builder, Scalar x, Scalar y)
Definition: tessellator.cc:24
ContentContextOptions OptionsFromPass(const RenderPass &pass)
Definition: contents.cc:19
ISize64 ISize
Definition: size.h:162
void Close(PathBuilder *builder)
Definition: tessellator.cc:38
std::shared_ptr< ContextGLES > context
std::shared_ptr< RenderPass > render_pass
Range GetRange() const
Definition: buffer_view.h:27
const DeviceBuffer * GetBuffer() const
Definition: buffer_view.cc:17
static constexpr Color LimeGreen()
Definition: color.h:607
static constexpr Color MintCream()
Definition: color.h:663
Scalar alpha
Definition: color.h:143
static constexpr Color DeepPink()
Definition: color.h:439
static constexpr Color Black()
Definition: color.h:271
static constexpr Color CornflowerBlue()
Definition: color.h:347
static constexpr Color White()
Definition: color.h:269
constexpr Color WithAlpha(Scalar new_alpha) const
Definition: color.h:283
static constexpr Color WhiteTransparent()
Definition: color.h:273
static constexpr Color Coral()
Definition: color.h:343
static constexpr Color Red()
Definition: color.h:277
constexpr Color Premultiply() const
Definition: color.h:212
Color Blend(Color source, BlendMode blend_mode) const
Blends an unpremultiplied destination color into a given unpremultiplied source color to form a new u...
Definition: color.cc:155
static constexpr Color Blue()
Definition: color.h:281
static constexpr Color Green()
Definition: color.h:279
Scalar array[20]
Definition: color.h:118
constexpr uint64_t ToKey() const
@ kNormal
The geometry has no overlapping triangles.
VertexBuffer vertex_buffer
Definition: geometry.h:38
A 4x4 matrix using column-major storage.
Definition: matrix.h:37
static constexpr Matrix MakeTranslation(const Vector3 &t)
Definition: matrix.h:95
constexpr bool IsIdentity() const
Definition: matrix.h:475
Matrix Invert() const
Definition: matrix.cc:99
static Matrix MakeRotationY(Radians r)
Definition: matrix.h:208
static constexpr Matrix MakeSkew(Scalar sx, Scalar sy)
Definition: matrix.h:127
static Matrix MakeRotationZ(Radians r)
Definition: matrix.h:223
static constexpr Matrix MakeScale(const Vector3 &s)
Definition: matrix.h:104
static Matrix MakeRotationX(Radians r)
Definition: matrix.h:193
For convolution filters, the "radius" is the size of the convolution kernel to use on the local space...
Definition: sigma.h:48
size_t offset
Definition: range.h:14
static RoundSuperellipse MakeRectRadii(const Rect &rect, const RoundingRadii &radii)
static RoundSuperellipse MakeRectRadius(const Rect &rect, Scalar radius)
In filters that use Gaussian distributions, "sigma" is a size of one standard deviation in terms of t...
Definition: sigma.h:32
A structure to store all of the parameters related to stroking a path or basic geometry object.
constexpr TRect< T > Expand(T left, T top, T right, T bottom) const
Returns a rectangle with expanded edges. Negative expansion results in shrinking.
Definition: rect.h:652
constexpr static TRect MakeXYWH(Type x, Type y, Type width, Type height)
Definition: rect.h:136
constexpr static TRect MakeSize(const TSize< U > &size)
Definition: rect.h:150
constexpr static TRect MakeEllipseBounds(const TPoint< Type > &center, const TSize< Type > &radii)
Definition: rect.h:164
constexpr static TRect MakeLTRB(Type left, Type top, Type right, Type bottom)
Definition: rect.h:129
A lightweight object that describes the attributes of a texture that can then used an allocator to cr...
static UberSDFParameters MakeCircle(Color color, const Point &center, Scalar radius, std::optional< StrokeParameters > stroke)
Creates UberSDFParameters for a circle.
static UberSDFParameters MakeRect(Color color, const Rect &rect, std::optional< StrokeParameters > stroke)
Creates UberSDFParameters for a rectangle.
const size_t start
std::vector< Point > points
Scalar font_size