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