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