Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
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
14#include "fml/logging.h"
15#include "gtest/gtest.h"
60#include "third_party/abseil-cpp/absl/status/status_matchers.h"
61#include "third_party/imgui/imgui.h"
62
63// TODO(zanderso): https://github.com/flutter/flutter/issues/127701
64// NOLINTBEGIN(bugprone-unchecked-optional-access)
65
66namespace impeller {
67namespace testing {
68
69using EntityTest = EntityPlayground;
71
72TEST_P(EntityTest, CanCreateEntity) {
73 Entity entity;
74 ASSERT_TRUE(entity.GetTransform().IsIdentity());
75}
76
77TEST_P(EntityTest, FilterCoverageRespectsCropRect) {
78 auto image = CreateTextureForFixture("boston.jpg");
81
82 // Without the crop rect (default behavior).
83 {
84 auto actual = filter->GetCoverage({});
85 auto expected = Rect::MakeSize(image->GetSize());
86
87 ASSERT_TRUE(actual.has_value());
88 ASSERT_RECT_NEAR(actual.value(), expected);
89 }
90
91 // With the crop rect.
92 {
93 auto expected = Rect::MakeLTRB(50, 50, 100, 100);
94 filter->SetCoverageHint(expected);
95 auto actual = filter->GetCoverage({});
96
97 ASSERT_TRUE(actual.has_value());
98 ASSERT_RECT_NEAR(actual.value(), expected);
99 }
100}
101
102TEST_P(EntityTest, GeometryBoundsAreTransformed) {
103 auto geometry = Geometry::MakeRect(Rect::MakeXYWH(100, 100, 100, 100));
104 auto transform = Matrix::MakeScale({2.0, 2.0, 2.0});
105
106 ASSERT_RECT_NEAR(geometry->GetCoverage(transform).value(),
107 Rect::MakeXYWH(200, 200, 200, 200));
108}
109
110TEST_P(EntityTest, ThreeStrokesInOnePath) {
112 .MoveTo({100, 100})
113 .LineTo({100, 200})
114 .MoveTo({100, 300})
115 .LineTo({100, 400})
116 .MoveTo({100, 500})
117 .LineTo({100, 600})
118 .TakePath();
119
120 Entity entity;
121 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
122 std::unique_ptr<Geometry> geom =
123 Geometry::MakeStrokePath(path, {.width = 5.0f});
124 auto contents = std::make_unique<SolidColorContents>(geom.get());
125 contents->SetColor(Color::Red());
126 entity.SetContents(std::move(contents));
127 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
128}
129
130TEST_P(EntityTest, StrokeWithTextureContents) {
131 auto bridge = CreateTextureForFixture("bay_bridge.jpg");
133 .MoveTo({100, 100})
134 .LineTo({100, 200})
135 .MoveTo({100, 300})
136 .LineTo({100, 400})
137 .MoveTo({100, 500})
138 .LineTo({100, 600})
139 .TakePath();
140
141 Entity entity;
142 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
143 std::unique_ptr<Geometry> geom =
144 Geometry::MakeStrokePath(path, {.width = 100.0f});
145 auto contents = std::make_unique<TiledTextureContents>(geom.get());
146 contents->SetTexture(bridge);
147 contents->SetTileModes(Entity::TileMode::kClamp, Entity::TileMode::kClamp);
148 entity.SetContents(std::move(contents));
149 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
150}
151
152TEST_P(EntityTest, TriangleInsideASquare) {
153 auto callback = [&](ContentContext& context, RenderPass& pass) {
154 Point offset(100, 100);
155
156 static PlaygroundPoint point_a(Point(10, 10) + offset, 20, Color::White());
157 Point a = DrawPlaygroundPoint(point_a);
158 static PlaygroundPoint point_b(Point(210, 10) + offset, 20, Color::White());
159 Point b = DrawPlaygroundPoint(point_b);
160 static PlaygroundPoint point_c(Point(210, 210) + offset, 20,
161 Color::White());
162 Point c = DrawPlaygroundPoint(point_c);
163 static PlaygroundPoint point_d(Point(10, 210) + offset, 20, Color::White());
164 Point d = DrawPlaygroundPoint(point_d);
165 static PlaygroundPoint point_e(Point(50, 50) + offset, 20, Color::White());
166 Point e = DrawPlaygroundPoint(point_e);
167 static PlaygroundPoint point_f(Point(100, 50) + offset, 20, Color::White());
168 Point f = DrawPlaygroundPoint(point_f);
169 static PlaygroundPoint point_g(Point(50, 150) + offset, 20, Color::White());
170 Point g = DrawPlaygroundPoint(point_g);
172 .MoveTo(a)
173 .LineTo(b)
174 .LineTo(c)
175 .LineTo(d)
176 .Close()
177 .MoveTo(e)
178 .LineTo(f)
179 .LineTo(g)
180 .Close()
181 .TakePath();
182
183 Entity entity;
184 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
185 std::unique_ptr<Geometry> geom =
186 Geometry::MakeStrokePath(path, {.width = 20.0});
187 auto contents = std::make_unique<SolidColorContents>(geom.get());
188 contents->SetColor(Color::Red());
189 entity.SetContents(std::move(contents));
190
191 return entity.Render(context, pass);
192 };
193 ASSERT_TRUE(OpenPlaygroundHere(callback));
194}
195
196TEST_P(EntityTest, StrokeCapAndJoinTest) {
197 const Point padding(300, 250);
198 const Point margin(140, 180);
199
200 auto callback = [&](ContentContext& context, RenderPass& pass) {
201 // Slightly above sqrt(2) by default, so that right angles are just below
202 // the limit and acute angles are over the limit (causing them to get
203 // beveled).
204 static Scalar miter_limit = 1.41421357;
205 static Scalar width = 30;
206
207 if (IsPlaygroundEnabled()) {
208 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
209 ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30);
210 ImGui::SliderFloat("Stroke width", &width, 0, 100);
211 if (ImGui::Button("Reset")) {
212 miter_limit = 1.41421357;
213 width = 30;
214 }
215 ImGui::End();
216 }
217
218 auto world_matrix = Matrix::MakeScale(GetContentScale());
219 auto render_path = [width = width, &context, &pass, &world_matrix](
220 const flutter::DlPath& path, Cap cap, Join join) {
221 std::unique_ptr<Geometry> geom =
223 .width = width,
224 .cap = cap,
225 .join = join,
226 .miter_limit = miter_limit,
227 });
228 auto contents = std::make_unique<SolidColorContents>(geom.get());
229 contents->SetColor(Color::Red());
230
231 Entity entity;
232 entity.SetTransform(world_matrix);
233 entity.SetContents(std::move(contents));
234
235 auto coverage = entity.GetCoverage();
236 if (coverage.has_value()) {
237 std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(
238 flutter::DlPath::MakeRect(entity.GetCoverage().value()));
239
240 auto bounds_contents = std::make_unique<SolidColorContents>(geom.get());
241 bounds_contents->SetColor(Color::Green().WithAlpha(0.5));
242 Entity bounds_entity;
243 bounds_entity.SetContents(std::move(bounds_contents));
244 bounds_entity.Render(context, pass);
245 }
246
247 entity.Render(context, pass);
248 };
249
250 const Point a_def(0, 0), b_def(0, 100), c_def(150, 0), d_def(150, -100),
251 e_def(75, 75);
252 const Scalar r = 30;
253 // Cap::kButt demo.
254 {
255 Point off = Point(0, 0) * padding + margin;
256 static PlaygroundPoint point_a(off + a_def, r, Color::Black());
257 static PlaygroundPoint point_b(off + b_def, r, Color::White());
258 auto [a, b] = DrawPlaygroundLine(point_a, point_b);
259 static PlaygroundPoint point_c(off + c_def, r, Color::Black());
260 static PlaygroundPoint point_d(off + d_def, r, Color::White());
261 auto [c, d] = DrawPlaygroundLine(point_c, point_d);
262 render_path(flutter::DlPathBuilder{} //
263 .MoveTo(a)
264 .CubicCurveTo(b, d, c)
265 .TakePath(),
267 }
268
269 // Cap::kSquare demo.
270 {
271 Point off = Point(1, 0) * padding + margin;
272 static PlaygroundPoint point_a(off + a_def, r, Color::Black());
273 static PlaygroundPoint point_b(off + b_def, r, Color::White());
274 auto [a, b] = DrawPlaygroundLine(point_a, point_b);
275 static PlaygroundPoint point_c(off + c_def, r, Color::Black());
276 static PlaygroundPoint point_d(off + d_def, r, Color::White());
277 auto [c, d] = DrawPlaygroundLine(point_c, point_d);
278 render_path(flutter::DlPathBuilder{} //
279 .MoveTo(a)
280 .CubicCurveTo(b, d, c)
281 .TakePath(),
283 }
284
285 // Cap::kRound demo.
286 {
287 Point off = Point(2, 0) * padding + margin;
288 static PlaygroundPoint point_a(off + a_def, r, Color::Black());
289 static PlaygroundPoint point_b(off + b_def, r, Color::White());
290 auto [a, b] = DrawPlaygroundLine(point_a, point_b);
291 static PlaygroundPoint point_c(off + c_def, r, Color::Black());
292 static PlaygroundPoint point_d(off + d_def, r, Color::White());
293 auto [c, d] = DrawPlaygroundLine(point_c, point_d);
294 render_path(flutter::DlPathBuilder{} //
295 .MoveTo(a)
296 .CubicCurveTo(b, d, c)
297 .TakePath(),
299 }
300
301 // Join::kBevel demo.
302 {
303 Point off = Point(0, 1) * padding + margin;
304 static PlaygroundPoint point_a =
305 PlaygroundPoint(off + a_def, r, Color::White());
306 static PlaygroundPoint point_b =
307 PlaygroundPoint(off + e_def, r, Color::White());
308 static PlaygroundPoint point_c =
309 PlaygroundPoint(off + c_def, r, Color::White());
310 Point a = DrawPlaygroundPoint(point_a);
311 Point b = DrawPlaygroundPoint(point_b);
312 Point c = DrawPlaygroundPoint(point_c);
313 render_path(flutter::DlPathBuilder{} //
314 .MoveTo(a)
315 .LineTo(b)
316 .LineTo(c)
317 .Close()
318 .TakePath(),
320 }
321
322 // Join::kMiter demo.
323 {
324 Point off = Point(1, 1) * padding + margin;
325 static PlaygroundPoint point_a(off + a_def, r, Color::White());
326 static PlaygroundPoint point_b(off + e_def, r, Color::White());
327 static PlaygroundPoint point_c(off + c_def, r, Color::White());
328 Point a = DrawPlaygroundPoint(point_a);
329 Point b = DrawPlaygroundPoint(point_b);
330 Point c = DrawPlaygroundPoint(point_c);
331 render_path(flutter::DlPathBuilder{} //
332 .MoveTo(a)
333 .LineTo(b)
334 .LineTo(c)
335 .Close()
336 .TakePath(),
338 }
339
340 // Join::kRound demo.
341 {
342 Point off = Point(2, 1) * padding + margin;
343 static PlaygroundPoint point_a(off + a_def, r, Color::White());
344 static PlaygroundPoint point_b(off + e_def, r, Color::White());
345 static PlaygroundPoint point_c(off + c_def, r, Color::White());
346 Point a = DrawPlaygroundPoint(point_a);
347 Point b = DrawPlaygroundPoint(point_b);
348 Point c = DrawPlaygroundPoint(point_c);
349 render_path(flutter::DlPathBuilder{} //
350 .MoveTo(a)
351 .LineTo(b)
352 .LineTo(c)
353 .Close()
354 .TakePath(),
356 }
357
358 return true;
359 };
360 ASSERT_TRUE(OpenPlaygroundHere(callback));
361}
362
363TEST_P(EntityTest, CubicCurveTest) {
364 // Compare with https://fiddle.skia.org/c/b3625f26122c9de7afe7794fcf25ead3
367 .MoveTo({237.164, 125.003})
368 .CubicCurveTo({236.709, 125.184}, {236.262, 125.358},
369 {235.81, 125.538})
370 .CubicCurveTo({235.413, 125.68}, {234.994, 125.832},
371 {234.592, 125.977})
372 .CubicCurveTo({234.592, 125.977}, {234.591, 125.977},
373 {234.59, 125.977})
374 .CubicCurveTo({222.206, 130.435}, {207.708, 135.753},
375 {192.381, 141.429})
376 .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160})
377 .Close()
378 .TakePath();
379 Entity entity;
380 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
381
382 std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
383
384 auto contents = std::make_shared<SolidColorContents>(geom.get());
385 contents->SetColor(Color::Red());
386
387 entity.SetContents(contents);
388 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
389}
390
391TEST_P(EntityTest, CanDrawCorrectlyWithRotatedTransform) {
392 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
393 const char* input_axis[] = {"X", "Y", "Z"};
394 static int rotation_axis_index = 0;
395 static float rotation = 0;
396 if (IsPlaygroundEnabled()) {
397 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
398 ImGui::SliderFloat("Rotation", &rotation, -kPi, kPi);
399 ImGui::Combo("Rotation Axis", &rotation_axis_index, input_axis,
400 sizeof(input_axis) / sizeof(char*));
401 if (ImGui::Button("Reset")) {
402 rotation = 0;
403 }
404 ImGui::End();
405 }
406 Matrix rotation_matrix;
407 switch (rotation_axis_index) {
408 case 0:
409 rotation_matrix = Matrix::MakeRotationX(Radians(rotation));
410 break;
411 case 1:
412 rotation_matrix = Matrix::MakeRotationY(Radians(rotation));
413 break;
414 case 2:
415 rotation_matrix = Matrix::MakeRotationZ(Radians(rotation));
416 break;
417 default:
418 rotation_matrix = Matrix{};
419 break;
420 }
421
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;
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
445TEST_P(EntityTest, CubicCurveAndOverlapTest) {
446 // Compare with https://fiddle.skia.org/c/7a05a3e186c65a8dfb732f68020aae06
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
679TEST_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
711TEST_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
739TEST_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) {
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);
763 blend_mode_names.push_back("SourceOver");
764 blend_mode_values.push_back(BlendMode::kSrcOver);
766 blend_mode_names.push_back("DestinationOver");
767 blend_mode_values.push_back(BlendMode::kDstOver);
769 blend_mode_names.push_back("SourceIn");
770 blend_mode_values.push_back(BlendMode::kSrcIn);
772 blend_mode_names.push_back("DestinationIn");
773 blend_mode_values.push_back(BlendMode::kDstIn);
775 blend_mode_names.push_back("SourceOut");
776 blend_mode_values.push_back(BlendMode::kSrcOut);
778 blend_mode_names.push_back("DestinationOut");
779 blend_mode_values.push_back(BlendMode::kDstOut);
781 blend_mode_names.push_back("SourceATop");
782 blend_mode_values.push_back(BlendMode::kSrcATop);
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 {
802 using VS = SolidFillPipeline::VertexShader;
803 using FS = SolidFillPipeline::FragmentShader;
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 static Color color1(1, 0, 0, 0.5), color2(0, 1, 0, 0.5);
839 static int current_blend_index = 3;
840 if (IsPlaygroundEnabled()) {
841 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
842 ImGui::ColorEdit4("Color 1", reinterpret_cast<float*>(&color1));
843 ImGui::ColorEdit4("Color 2", reinterpret_cast<float*>(&color2));
844 ImGui::ListBox("Blending mode", &current_blend_index,
845 blend_mode_names.data(), blend_mode_names.size());
846 ImGui::End();
847 }
848
849 BlendMode selected_mode = blend_mode_values[current_blend_index];
850
851 Point a, b, c, d;
852 static PlaygroundPoint point_a(Point(400, 100), 20, Color::White());
853 static PlaygroundPoint point_b(Point(200, 300), 20, Color::White());
854 std::tie(a, b) = DrawPlaygroundLine(point_a, point_b);
855 static PlaygroundPoint point_c(Point(470, 190), 20, Color::White());
856 static PlaygroundPoint point_d(Point(270, 390), 20, Color::White());
857 std::tie(c, d) = DrawPlaygroundLine(point_c, point_d);
858
859 bool result = true;
860 result = result &&
861 draw_rect(Rect::MakeXYWH(0, 0, pass.GetRenderTargetSize().width,
862 pass.GetRenderTargetSize().height),
864 result = result && draw_rect(Rect::MakeLTRB(a.x, a.y, b.x, b.y), color1,
866 result = result && draw_rect(Rect::MakeLTRB(c.x, c.y, d.x, d.y), color2,
867 selected_mode);
868 return result;
869 };
870 ASSERT_TRUE(OpenPlaygroundHere(callback));
871}
872
873TEST_P(EntityTest, BezierCircleScaled) {
874 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
875 static float scale = 20;
876
877 if (IsPlaygroundEnabled()) {
878 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
879 ImGui::SliderFloat("Scale", &scale, 1, 100);
880 ImGui::End();
881 }
882
883 Entity entity;
884 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
886 .MoveTo({97.325, 34.818})
887 .CubicCurveTo({98.50862885295136, 34.81812293973836},
888 {99.46822048142015, 33.85863261475589},
889 {99.46822048142015, 32.67499810206613})
890 .CubicCurveTo({99.46822048142015, 31.491363589376355},
891 {98.50862885295136, 30.53187326439389},
892 {97.32499434685802, 30.531998226542708})
893 .CubicCurveTo({96.14153655073771, 30.532123170035373},
894 {95.18222070648729, 31.491540299350355},
895 {95.18222070648729, 32.67499810206613})
896 .CubicCurveTo({95.18222070648729, 33.85845590478189},
897 {96.14153655073771, 34.81787303409686},
898 {97.32499434685802, 34.81799797758954})
899 .Close()
900 .TakePath();
901 entity.SetTransform(
902 Matrix::MakeScale({scale, scale, 1.0}).Translate({-90, -20, 0}));
903
904 std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
905
906 auto contents = std::make_shared<SolidColorContents>(geom.get());
907 contents->SetColor(Color::Red());
908
909 entity.SetContents(contents);
910 return entity.Render(context, pass);
911 };
912 ASSERT_TRUE(OpenPlaygroundHere(callback));
913}
914
916 auto bridge = CreateTextureForFixture("bay_bridge.jpg");
917 auto boston = CreateTextureForFixture("boston.jpg");
918 auto kalimba = CreateTextureForFixture("kalimba.jpg");
919 ASSERT_TRUE(bridge && boston && kalimba);
920
921 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
922 auto fi_bridge = FilterInput::Make(bridge);
923 auto fi_boston = FilterInput::Make(boston);
924 auto fi_kalimba = FilterInput::Make(kalimba);
925
926 std::shared_ptr<FilterContents> blend0 = ColorFilterContents::MakeBlend(
927 BlendMode::kModulate, {fi_kalimba, fi_boston});
928
929 auto blend1 = ColorFilterContents::MakeBlend(
931 {FilterInput::Make(blend0), fi_bridge, fi_bridge, fi_bridge});
932
933 Entity entity;
934 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
935 Matrix::MakeTranslation({500, 300}) *
936 Matrix::MakeScale(Vector2{0.5, 0.5}));
937 entity.SetContents(blend1);
938 return entity.Render(context, pass);
939 };
940 ASSERT_TRUE(OpenPlaygroundHere(callback));
941}
942
943TEST_P(EntityTest, GaussianBlurFilter) {
944 auto boston =
945 CreateTextureForFixture("boston.jpg", /*enable_mipmapping=*/true);
946 ASSERT_TRUE(boston);
947
948 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
949 const char* input_type_names[] = {"Texture", "Solid Color"};
950 const char* blur_type_names[] = {"Image blur", "Mask blur"};
951 const char* pass_variation_names[] = {"New"};
952 const char* blur_style_names[] = {"Normal", "Solid", "Outer", "Inner"};
953 const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"};
954 const FilterContents::BlurStyle blur_styles[] = {
957 const Entity::TileMode tile_modes[] = {
960
961 // UI state.
962 static int selected_input_type = 0;
963 static Color input_color = Color::Black();
964 static int selected_blur_type = 0;
965 static int selected_pass_variation = 0;
966 static bool combined_sigma = false;
967 static float blur_amount_coarse[2] = {0, 0};
968 static float blur_amount_fine[2] = {10, 10};
969 static int selected_blur_style = 0;
970 static int selected_tile_mode = 3;
971 static Color cover_color(1, 0, 0, 0.2);
972 static Color bounds_color(0, 1, 0, 0.1);
973 static float offset[2] = {500, 400};
974 static float rotation = 0;
975 static float scale[2] = {0.65, 0.65};
976 static float skew[2] = {0, 0};
977 static float path_rect[4] = {0, 0,
978 static_cast<float>(boston->GetSize().width),
979 static_cast<float>(boston->GetSize().height)};
980
981 if (IsPlaygroundEnabled()) {
982 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
983 ImGui::Combo("Input type", &selected_input_type, input_type_names,
984 sizeof(input_type_names) / sizeof(char*));
985 if (selected_input_type == 0) {
986 ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1);
987 } else {
988 ImGui::ColorEdit4("Input color",
989 reinterpret_cast<float*>(&input_color));
990 }
991 ImGui::Combo("Blur type", &selected_blur_type, blur_type_names,
992 sizeof(blur_type_names) / sizeof(char*));
993 if (selected_blur_type == 0) {
994 ImGui::Combo("Pass variation", &selected_pass_variation,
995 pass_variation_names,
996 sizeof(pass_variation_names) / sizeof(char*));
997 }
998 ImGui::Checkbox("Combined sigma", &combined_sigma);
999 if (combined_sigma) {
1000 ImGui::SliderFloat("Sigma (coarse)", blur_amount_coarse, 0, 1000);
1001 ImGui::SliderFloat("Sigma (fine)", blur_amount_fine, 0, 10);
1002 blur_amount_coarse[1] = blur_amount_coarse[0];
1003 blur_amount_fine[1] = blur_amount_fine[0];
1004 } else {
1005 ImGui::SliderFloat2("Sigma (coarse)", blur_amount_coarse, 0, 1000);
1006 ImGui::SliderFloat2("Sigma (fine)", blur_amount_fine, 0, 10);
1007 }
1008 ImGui::Combo("Blur style", &selected_blur_style, blur_style_names,
1009 sizeof(blur_style_names) / sizeof(char*));
1010 ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names,
1011 sizeof(tile_mode_names) / sizeof(char*));
1012 ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color));
1013 ImGui::ColorEdit4("Bounds color ",
1014 reinterpret_cast<float*>(&bounds_color));
1015 ImGui::SliderFloat2("Translation", offset, 0,
1016 pass.GetRenderTargetSize().width);
1017 ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1018 ImGui::SliderFloat2("Scale", scale, 0, 3);
1019 ImGui::SliderFloat2("Skew", skew, -3, 3);
1020 ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000);
1021 ImGui::End();
1022 }
1023
1024 auto blur_sigma_x = Sigma{blur_amount_coarse[0] + blur_amount_fine[0]};
1025 auto blur_sigma_y = Sigma{blur_amount_coarse[1] + blur_amount_fine[1]};
1026
1027 std::shared_ptr<Contents> input;
1028 Size input_size;
1029
1030 auto input_rect =
1031 Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]);
1032
1033 std::unique_ptr<Geometry> solid_color_input;
1034 if (selected_input_type == 0) {
1035 auto texture = std::make_shared<TextureContents>();
1036 texture->SetSourceRect(Rect::MakeSize(boston->GetSize()));
1037 texture->SetDestinationRect(input_rect);
1038 texture->SetTexture(boston);
1039 texture->SetOpacity(input_color.alpha);
1040
1041 input = texture;
1042 input_size = input_rect.GetSize();
1043 } else {
1044 solid_color_input =
1046 auto fill = std::make_shared<SolidColorContents>(solid_color_input.get());
1047 fill->SetColor(input_color);
1048
1049 input = fill;
1050 input_size = input_rect.GetSize();
1051 }
1052
1053 std::shared_ptr<FilterContents> blur;
1054 switch (selected_pass_variation) {
1055 case 0:
1056 blur = std::make_shared<GaussianBlurFilterContents>(
1057 blur_sigma_x.sigma, blur_sigma_y.sigma,
1058 tile_modes[selected_tile_mode], /*bounds=*/std::nullopt,
1059 blur_styles[selected_blur_style],
1060 /*geometry=*/nullptr);
1061 blur->SetInputs({FilterInput::Make(input)});
1062 break;
1063 case 1:
1065 FilterInput::Make(input), blur_sigma_x, blur_sigma_y,
1066 tile_modes[selected_tile_mode],
1067 /*bounds=*/std::nullopt, blur_styles[selected_blur_style]);
1068 break;
1069 };
1070 FML_CHECK(blur);
1071
1072 auto mask_blur = FilterContents::MakeBorderMaskBlur(
1073 FilterInput::Make(input), blur_sigma_x, blur_sigma_y,
1074 blur_styles[selected_blur_style]);
1075
1076 auto ctm = Matrix::MakeScale(GetContentScale()) *
1077 Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1078 Matrix::MakeRotationZ(Radians(rotation)) *
1079 Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1080 Matrix::MakeSkew(skew[0], skew[1]) *
1081 Matrix::MakeTranslation(-Point(input_size) / 2);
1082
1083 auto target_contents = selected_blur_type == 0 ? blur : mask_blur;
1084
1085 Entity entity;
1086 entity.SetContents(target_contents);
1087 entity.SetTransform(ctm);
1088
1089 entity.Render(context, pass);
1090
1091 // Renders a red "cover" rectangle that shows the original position of the
1092 // unfiltered input.
1093 Entity cover_entity;
1094 auto geom = Geometry::MakeFillPath(flutter::DlPath::MakeRect(input_rect));
1095 auto contents = std::make_shared<SolidColorContents>(geom.get());
1096 contents->SetColor(cover_color);
1097 cover_entity.SetContents(std::move(contents));
1098 cover_entity.SetTransform(ctm);
1099 cover_entity.Render(context, pass);
1100
1101 // Renders a green bounding rect of the target filter.
1102 Entity bounds_entity;
1103 std::optional<Rect> target_contents_coverage =
1104 target_contents->GetCoverage(entity);
1105 if (target_contents_coverage.has_value()) {
1106 std::unique_ptr<Geometry> geom =
1108 target_contents->GetCoverage(entity).value()));
1109 auto contents = std::make_shared<SolidColorContents>(geom.get());
1110 contents->SetColor(bounds_color);
1111
1112 bounds_entity.SetContents(contents);
1113 bounds_entity.SetTransform(Matrix());
1114 bounds_entity.Render(context, pass);
1115 }
1116
1117 return true;
1118 };
1119 ASSERT_TRUE(OpenPlaygroundHere(callback));
1120}
1121
1122TEST_P(EntityTest, MorphologyFilter) {
1123 auto boston = CreateTextureForFixture("boston.jpg");
1124 ASSERT_TRUE(boston);
1125
1126 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1127 const char* morphology_type_names[] = {"Dilate", "Erode"};
1128 const FilterContents::MorphType morphology_types[] = {
1130 static Color input_color = Color::Black();
1131 // UI state.
1132 static int selected_morphology_type = 0;
1133 static float radius[2] = {20, 20};
1134 static Color cover_color(1, 0, 0, 0.2);
1135 static Color bounds_color(0, 1, 0, 0.1);
1136 static float offset[2] = {500, 400};
1137 static float rotation = 0;
1138 static float scale[2] = {0.65, 0.65};
1139 static float skew[2] = {0, 0};
1140 static float path_rect[4] = {0, 0,
1141 static_cast<float>(boston->GetSize().width),
1142 static_cast<float>(boston->GetSize().height)};
1143 static float effect_transform_scale = 1;
1144
1145 if (IsPlaygroundEnabled()) {
1146 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1147 ImGui::Combo("Morphology type", &selected_morphology_type,
1148 morphology_type_names,
1149 sizeof(morphology_type_names) / sizeof(char*));
1150 ImGui::SliderFloat2("Radius", radius, 0, 200);
1151 ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1);
1152 ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color));
1153 ImGui::ColorEdit4("Bounds color ",
1154 reinterpret_cast<float*>(&bounds_color));
1155 ImGui::SliderFloat2("Translation", offset, 0,
1156 pass.GetRenderTargetSize().width);
1157 ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1158 ImGui::SliderFloat2("Scale", scale, 0, 3);
1159 ImGui::SliderFloat2("Skew", skew, -3, 3);
1160 ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000);
1161 ImGui::SliderFloat("Effect transform scale", &effect_transform_scale, 0,
1162 3);
1163 ImGui::End();
1164 }
1165
1166 std::shared_ptr<Contents> input;
1167 Size input_size;
1168
1169 auto input_rect =
1170 Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]);
1171 auto texture = std::make_shared<TextureContents>();
1172 texture->SetSourceRect(Rect::MakeSize(boston->GetSize()));
1173 texture->SetDestinationRect(input_rect);
1174 texture->SetTexture(boston);
1175 texture->SetOpacity(input_color.alpha);
1176
1177 input = texture;
1178 input_size = input_rect.GetSize();
1179
1180 auto contents = FilterContents::MakeMorphology(
1181 FilterInput::Make(input), Radius{radius[0]}, Radius{radius[1]},
1182 morphology_types[selected_morphology_type]);
1183 contents->SetEffectTransform(Matrix::MakeScale(
1184 Vector2{effect_transform_scale, effect_transform_scale}));
1185
1186 auto ctm = Matrix::MakeScale(GetContentScale()) *
1187 Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1188 Matrix::MakeRotationZ(Radians(rotation)) *
1189 Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1190 Matrix::MakeSkew(skew[0], skew[1]) *
1191 Matrix::MakeTranslation(-Point(input_size) / 2);
1192
1193 Entity entity;
1194 entity.SetContents(contents);
1195 entity.SetTransform(ctm);
1196
1197 entity.Render(context, pass);
1198
1199 // Renders a red "cover" rectangle that shows the original position of the
1200 // unfiltered input.
1201 Entity cover_entity;
1202 auto geom = Geometry::MakeFillPath(flutter::DlPath::MakeRect(input_rect));
1203 auto cover_contents = std::make_shared<SolidColorContents>(geom.get());
1204 cover_contents->SetColor(cover_color);
1205 cover_entity.SetContents(cover_contents);
1206 cover_entity.SetTransform(ctm);
1207 cover_entity.Render(context, pass);
1208
1209 // Renders a green bounding rect of the target filter.
1210 std::optional<Rect> contents_coverage = contents->GetCoverage(entity);
1211 if (contents_coverage.has_value()) {
1212 std::unique_ptr<Geometry> bounds_geom = Geometry::MakeFillPath(
1213 flutter::DlPath::MakeRect(contents_coverage.value()));
1214 auto bounds_contents =
1215 std::make_shared<SolidColorContents>(bounds_geom.get());
1216 bounds_contents->SetColor(bounds_color);
1217 Entity bounds_entity;
1218 bounds_entity.SetContents(std::move(bounds_contents));
1219 bounds_entity.SetTransform(Matrix());
1220
1221 bounds_entity.Render(context, pass);
1222 }
1223
1224 return true;
1225 };
1226 ASSERT_TRUE(OpenPlaygroundHere(callback));
1227}
1228
1229TEST_P(EntityTest, SetBlendMode) {
1230 Entity entity;
1231 ASSERT_EQ(entity.GetBlendMode(), BlendMode::kSrcOver);
1233 ASSERT_EQ(entity.GetBlendMode(), BlendMode::kClear);
1234}
1235
1236TEST_P(EntityTest, ContentsGetBoundsForEmptyPathReturnsNullopt) {
1237 Entity entity;
1238 entity.SetContents(std::make_shared<SolidColorContents>(nullptr));
1239 ASSERT_FALSE(entity.GetCoverage().has_value());
1240}
1241
1242TEST(EntityTest, UberSDFContentsCoverageFillRect) {
1243 auto rect = Rect::MakeXYWH(100, 100, 200, 200);
1244 auto params =
1245 UberSDFParameters::MakeRect(Color::Red(), rect, /*stroke=*/std::nullopt);
1246 auto geometry = std::make_unique<UberSDFGeometry>(params);
1247 auto contents = UberSDFContents::Make(params, std::move(geometry));
1248
1249 Entity entity;
1250 auto coverage = contents->GetCoverage(entity);
1251 ASSERT_TRUE(coverage.has_value());
1253 coverage.value(),
1254 Rect::MakeXYWH(100, 100, 200, 200).Expand(1.0f)); // expanded by AA
1255}
1256
1257TEST(EntityTest, UberSDFContentsCoverageStrokeRect) {
1258 auto rect = Rect::MakeXYWH(100, 100, 200, 200);
1260 StrokeParameters{.width = 4.0f});
1261 auto geometry = std::make_unique<UberSDFGeometry>(params);
1262 auto contents = UberSDFContents::Make(params, std::move(geometry));
1263
1264 Entity entity;
1265 auto coverage = contents->GetCoverage(entity);
1266 ASSERT_TRUE(coverage.has_value());
1267 ASSERT_RECT_NEAR(coverage.value(),
1268 Rect::MakeXYWH(100, 100, 200, 200)
1269 .Expand(3.0f)); // expanded by half stroke width + AA
1270}
1271
1272TEST(EntityTest, UberSDFContentsCoverageFillCircle) {
1273 auto params =
1274 UberSDFParameters::MakeCircle(Color::Red(), /*center=*/{50, 50},
1275 /*radius=*/10.0f, /*stroke=*/std::nullopt);
1276 auto geometry = std::make_unique<UberSDFGeometry>(params);
1277 auto contents = UberSDFContents::Make(params, std::move(geometry));
1278
1279 Entity entity;
1280 auto coverage = contents->GetCoverage(entity);
1281 ASSERT_TRUE(coverage.has_value());
1283 coverage.value(),
1284 Rect::MakeXYWH(40, 40, 20, 20).Expand(1.0f)); // expanded by AA
1285}
1286
1287TEST(EntityTest, UberSDFContentsCoverageStrokeCircle) {
1288 auto params = UberSDFParameters::MakeCircle(Color::Red(), /*center=*/{50, 50},
1289 /*radius=*/10.0f,
1290 StrokeParameters{.width = 4.0f});
1291 auto geometry = std::make_unique<UberSDFGeometry>(params);
1292 auto contents = UberSDFContents::Make(params, std::move(geometry));
1293
1294 Entity entity;
1295 auto coverage = contents->GetCoverage(entity);
1296 ASSERT_TRUE(coverage.has_value());
1297 ASSERT_RECT_NEAR(coverage.value(),
1298 Rect::MakeXYWH(40, 40, 20, 20)
1299 .Expand(3.0f)); // expanded by half stroke width + AA
1300}
1301
1302TEST_P(EntityTest, SolidStrokeCoverageIsCorrect) {
1303 {
1304 auto geometry = Geometry::MakeStrokePath(
1305 flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1306 {
1307 .width = 4.0f,
1308 .cap = Cap::kButt,
1309 .join = Join::kBevel,
1310 .miter_limit = 4.0f,
1311 });
1312
1313 Entity entity;
1314 auto contents = std::make_unique<SolidColorContents>(geometry.get());
1315 contents->SetColor(Color::Black());
1316 entity.SetContents(std::move(contents));
1317 auto actual = entity.GetCoverage();
1318 auto expected = Rect::MakeLTRB(-2, -2, 12, 12);
1319
1320 ASSERT_TRUE(actual.has_value());
1321 ASSERT_RECT_NEAR(actual.value(), expected);
1322 }
1323
1324 // Cover the Cap::kSquare case.
1325 {
1326 auto geometry = Geometry::MakeStrokePath(
1327 flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1328 {
1329 .width = 4.0,
1330 .cap = Cap::kSquare,
1331 .join = Join::kBevel,
1332 .miter_limit = 4.0,
1333 });
1334
1335 Entity entity;
1336 auto contents = std::make_unique<SolidColorContents>(geometry.get());
1337 contents->SetColor(Color::Black());
1338 entity.SetContents(std::move(contents));
1339 auto actual = entity.GetCoverage();
1340 auto expected =
1341 Rect::MakeLTRB(-sqrt(8), -sqrt(8), 10 + sqrt(8), 10 + sqrt(8));
1342
1343 ASSERT_TRUE(actual.has_value());
1344 ASSERT_RECT_NEAR(actual.value(), expected);
1345 }
1346
1347 // Cover the Join::kMiter case.
1348 {
1349 auto geometry = Geometry::MakeStrokePath(
1350 flutter::DlPath::MakeLine({0, 0}, {10, 10}), //
1351 {
1352 .width = 4.0f,
1353 .cap = Cap::kSquare,
1354 .join = Join::kMiter,
1355 .miter_limit = 2.0f,
1356 });
1357
1358 Entity entity;
1359 auto contents = std::make_unique<SolidColorContents>(geometry.get());
1360 contents->SetColor(Color::Black());
1361 entity.SetContents(std::move(contents));
1362 auto actual = entity.GetCoverage();
1363 auto expected = Rect::MakeLTRB(-4, -4, 14, 14);
1364
1365 ASSERT_TRUE(actual.has_value());
1366 ASSERT_RECT_NEAR(actual.value(), expected);
1367 }
1368}
1369
1370TEST_P(EntityTest, BorderMaskBlurCoverageIsCorrect) {
1371 auto geom = Geometry::MakeFillPath(
1372 flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1373 auto fill = std::make_shared<SolidColorContents>(geom.get());
1374 fill->SetColor(Color::CornflowerBlue());
1375 auto border_mask_blur = FilterContents::MakeBorderMaskBlur(
1376 FilterInput::Make(fill), Radius{3}, Radius{4});
1377
1378 {
1379 Entity e;
1380 e.SetTransform(Matrix());
1381 auto actual = border_mask_blur->GetCoverage(e);
1382 auto expected = Rect::MakeXYWH(-3, -4, 306, 408);
1383 ASSERT_TRUE(actual.has_value());
1384 ASSERT_RECT_NEAR(actual.value(), expected);
1385 }
1386
1387 {
1388 Entity e;
1390 auto actual = border_mask_blur->GetCoverage(e);
1391 auto expected = Rect::MakeXYWH(-287.792, -4.94975, 504.874, 504.874);
1392 ASSERT_TRUE(actual.has_value());
1393 ASSERT_RECT_NEAR(actual.value(), expected);
1394 }
1395}
1396
1397TEST_P(EntityTest, SolidFillCoverageIsCorrect) {
1398 // No transform
1399 {
1400 auto expected = Rect::MakeLTRB(100, 110, 200, 220);
1402 auto fill = std::make_shared<SolidColorContents>(geom.get());
1403 fill->SetColor(Color::CornflowerBlue());
1404
1405 auto coverage = fill->GetCoverage({});
1406 ASSERT_TRUE(coverage.has_value());
1407 ASSERT_RECT_NEAR(coverage.value(), expected);
1408 }
1409
1410 // Entity transform
1411 {
1412 auto geom = Geometry::MakeFillPath(
1413 flutter::DlPath::MakeRect(Rect::MakeLTRB(100, 110, 200, 220)));
1414 auto fill = std::make_shared<SolidColorContents>(geom.get());
1415 fill->SetColor(Color::CornflowerBlue());
1416
1417 Entity entity;
1419 entity.SetContents(std::move(fill));
1420
1421 auto coverage = entity.GetCoverage();
1422 auto expected = Rect::MakeLTRB(104, 115, 204, 225);
1423 ASSERT_TRUE(coverage.has_value());
1424 ASSERT_RECT_NEAR(coverage.value(), expected);
1425 }
1426
1427 // No coverage for fully transparent colors
1428 {
1429 auto geom = Geometry::MakeFillPath(
1430 flutter::DlPath::MakeRect(Rect::MakeLTRB(100, 110, 200, 220)));
1431 auto fill = std::make_shared<SolidColorContents>(geom.get());
1432 fill->SetColor(Color::WhiteTransparent());
1433
1434 auto coverage = fill->GetCoverage({});
1435 ASSERT_FALSE(coverage.has_value());
1436 }
1437}
1438
1439TEST_P(EntityTest, RRectShadowTest) {
1440 auto callback = [&](ContentContext& context, RenderPass& pass) {
1441 static Color color = Color::Red();
1442 static float corner_radius = 100;
1443 static float blur_radius = 100;
1444 static bool show_coverage = false;
1445 static Color coverage_color = Color::Green().WithAlpha(0.2);
1446 static PlaygroundPoint top_left_point(Point(200, 200), 30, Color::White());
1447 static PlaygroundPoint bottom_right_point(Point(600, 400), 30,
1448 Color::White());
1449
1450 if (IsPlaygroundEnabled()) {
1451 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1452 ImGui::SliderFloat("Corner radius", &corner_radius, 0, 300);
1453 ImGui::SliderFloat("Blur radius", &blur_radius, 0, 300);
1454 ImGui::ColorEdit4("Color", reinterpret_cast<Scalar*>(&color));
1455 ImGui::Checkbox("Show coverage", &show_coverage);
1456 if (show_coverage) {
1457 ImGui::ColorEdit4("Coverage color",
1458 reinterpret_cast<Scalar*>(&coverage_color));
1459 }
1460 ImGui::End();
1461 }
1462
1463 auto [top_left, bottom_right] =
1464 DrawPlaygroundLine(top_left_point, bottom_right_point);
1465 auto rect =
1466 Rect::MakeLTRB(top_left.x, top_left.y, bottom_right.x, bottom_right.y);
1467
1468 auto contents = std::make_unique<SolidRRectBlurContents>();
1469 contents->SetShape(rect, corner_radius);
1470 contents->SetColor(color);
1471 contents->SetSigma(Radius(blur_radius));
1472
1473 Entity entity;
1474 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
1475 entity.SetContents(std::move(contents));
1476 entity.Render(context, pass);
1477
1478 auto coverage = entity.GetCoverage();
1479 if (show_coverage && coverage.has_value()) {
1480 auto geom = Geometry::MakeFillPath(
1481 flutter::DlPath::MakeRect(entity.GetCoverage().value()));
1482 auto bounds_contents = std::make_unique<SolidColorContents>(geom.get());
1483 bounds_contents->SetColor(coverage_color.Premultiply());
1484 Entity bounds_entity;
1485 bounds_entity.SetContents(std::move(bounds_contents));
1486 bounds_entity.Render(context, pass);
1487 }
1488
1489 return true;
1490 };
1491 ASSERT_TRUE(OpenPlaygroundHere(callback));
1492}
1493
1494TEST_P(EntityTest, ColorMatrixFilterCoverageIsCorrect) {
1495 // Set up a simple color background.
1496 auto geom = Geometry::MakeFillPath(
1497 flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1498 auto fill = std::make_shared<SolidColorContents>(geom.get());
1499 fill->SetColor(Color::Coral());
1500
1501 // Set the color matrix filter.
1502 ColorMatrix matrix = {
1503 1, 1, 1, 1, 1, //
1504 1, 1, 1, 1, 1, //
1505 1, 1, 1, 1, 1, //
1506 1, 1, 1, 1, 1, //
1507 };
1508
1509 auto filter =
1511
1512 Entity e;
1513 e.SetTransform(Matrix());
1514
1515 // Confirm that the actual filter coverage matches the expected coverage.
1516 auto actual = filter->GetCoverage(e);
1517 auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1518
1519 ASSERT_TRUE(actual.has_value());
1520 ASSERT_RECT_NEAR(actual.value(), expected);
1521}
1522
1523TEST_P(EntityTest, ColorMatrixFilterEditable) {
1524 auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg");
1525 ASSERT_TRUE(bay_bridge);
1526
1527 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1528 // UI state.
1529 static ColorMatrix color_matrix = {
1530 1, 0, 0, 0, 0, //
1531 0, 3, 0, 0, 0, //
1532 0, 0, 1, 0, 0, //
1533 0, 0, 0, 1, 0, //
1534 };
1535 static float offset[2] = {500, 400};
1536 static float rotation = 0;
1537 static float scale[2] = {0.65, 0.65};
1538 static float skew[2] = {0, 0};
1539
1540 // Define the ImGui
1541 if (IsPlaygroundEnabled()) {
1542 ImGui::Begin("Color Matrix", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
1543 std::string label = "##1";
1544 for (int i = 0; i < 20; i += 5) {
1545 ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float,
1546 &(color_matrix.array[i]), 5, nullptr, nullptr,
1547 "%.2f", 0);
1548 label[2]++;
1549 }
1550
1551 ImGui::SliderFloat2("Translation", &offset[0], 0,
1552 pass.GetRenderTargetSize().width);
1553 ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2);
1554 ImGui::SliderFloat2("Scale", &scale[0], 0, 3);
1555 ImGui::SliderFloat2("Skew", &skew[0], -3, 3);
1556 ImGui::End();
1557 }
1558
1559 // Set the color matrix filter.
1561 FilterInput::Make(bay_bridge), color_matrix);
1562
1563 // Define the entity with the color matrix filter.
1564 Entity entity;
1565 entity.SetTransform(
1566 Matrix::MakeScale(GetContentScale()) *
1567 Matrix::MakeTranslation(Vector3(offset[0], offset[1])) *
1568 Matrix::MakeRotationZ(Radians(rotation)) *
1569 Matrix::MakeScale(Vector2(scale[0], scale[1])) *
1570 Matrix::MakeSkew(skew[0], skew[1]) *
1571 Matrix::MakeTranslation(-Point(bay_bridge->GetSize()) / 2));
1572 entity.SetContents(filter);
1573 entity.Render(context, pass);
1574
1575 return true;
1576 };
1577
1578 ASSERT_TRUE(OpenPlaygroundHere(callback));
1579}
1580
1581TEST_P(EntityTest, LinearToSrgbFilterCoverageIsCorrect) {
1582 // Set up a simple color background.
1583 auto geom = Geometry::MakeFillPath(
1584 flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1585 auto fill = std::make_shared<SolidColorContents>(geom.get());
1586 fill->SetColor(Color::MintCream());
1587
1588 auto filter =
1590
1591 Entity e;
1592 e.SetTransform(Matrix());
1593
1594 // Confirm that the actual filter coverage matches the expected coverage.
1595 auto actual = filter->GetCoverage(e);
1596 auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1597
1598 ASSERT_TRUE(actual.has_value());
1599 ASSERT_RECT_NEAR(actual.value(), expected);
1600}
1601
1602TEST_P(EntityTest, LinearToSrgbFilter) {
1603 auto image = CreateTextureForFixture("kalimba.jpg");
1604 ASSERT_TRUE(image);
1605
1606 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1607 auto filtered =
1609
1610 // Define the entity that will serve as the control image as a Gaussian blur
1611 // filter with no filter at all.
1612 Entity entity_left;
1613 entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) *
1614 Matrix::MakeTranslation({100, 300}) *
1615 Matrix::MakeScale(Vector2{0.5, 0.5}));
1617 Sigma{0}, Sigma{0});
1618 entity_left.SetContents(unfiltered);
1619
1620 // Define the entity that will be filtered from linear to sRGB.
1621 Entity entity_right;
1622 entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) *
1623 Matrix::MakeTranslation({500, 300}) *
1624 Matrix::MakeScale(Vector2{0.5, 0.5}));
1625 entity_right.SetContents(filtered);
1626 return entity_left.Render(context, pass) &&
1627 entity_right.Render(context, pass);
1628 };
1629
1630 ASSERT_TRUE(OpenPlaygroundHere(callback));
1631}
1632
1633TEST_P(EntityTest, SrgbToLinearFilterCoverageIsCorrect) {
1634 // Set up a simple color background.
1635 auto geom = Geometry::MakeFillPath(
1636 flutter::DlPath::MakeRect(Rect::MakeXYWH(0, 0, 300, 400)));
1637 auto fill = std::make_shared<SolidColorContents>(geom.get());
1638 fill->SetColor(Color::DeepPink());
1639
1640 auto filter =
1642
1643 Entity e;
1644 e.SetTransform(Matrix());
1645
1646 // Confirm that the actual filter coverage matches the expected coverage.
1647 auto actual = filter->GetCoverage(e);
1648 auto expected = Rect::MakeXYWH(0, 0, 300, 400);
1649
1650 ASSERT_TRUE(actual.has_value());
1651 ASSERT_RECT_NEAR(actual.value(), expected);
1652}
1653
1654TEST_P(EntityTest, SrgbToLinearFilter) {
1655 auto image = CreateTextureForFixture("embarcadero.jpg");
1656 ASSERT_TRUE(image);
1657
1658 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1659 auto filtered =
1661
1662 // Define the entity that will serve as the control image as a Gaussian blur
1663 // filter with no filter at all.
1664 Entity entity_left;
1665 entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) *
1666 Matrix::MakeTranslation({100, 300}) *
1667 Matrix::MakeScale(Vector2{0.5, 0.5}));
1669 Sigma{0}, Sigma{0});
1670 entity_left.SetContents(unfiltered);
1671
1672 // Define the entity that will be filtered from sRGB to linear.
1673 Entity entity_right;
1674 entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) *
1675 Matrix::MakeTranslation({500, 300}) *
1676 Matrix::MakeScale(Vector2{0.5, 0.5}));
1677 entity_right.SetContents(filtered);
1678 return entity_left.Render(context, pass) &&
1679 entity_right.Render(context, pass);
1680 };
1681
1682 ASSERT_TRUE(OpenPlaygroundHere(callback));
1683}
1684
1685static Vector3 RGBToYUV(Vector3 rgb, YUVColorSpace yuv_color_space) {
1686 Vector3 yuv;
1687 switch (yuv_color_space) {
1689 yuv.x = rgb.x * 0.299 + rgb.y * 0.587 + rgb.z * 0.114;
1690 yuv.y = rgb.x * -0.169 + rgb.y * -0.331 + rgb.z * 0.5 + 0.5;
1691 yuv.z = rgb.x * 0.5 + rgb.y * -0.419 + rgb.z * -0.081 + 0.5;
1692 break;
1694 yuv.x = rgb.x * 0.257 + rgb.y * 0.516 + rgb.z * 0.100 + 0.063;
1695 yuv.y = rgb.x * -0.145 + rgb.y * -0.291 + rgb.z * 0.439 + 0.5;
1696 yuv.z = rgb.x * 0.429 + rgb.y * -0.368 + rgb.z * -0.071 + 0.5;
1697 break;
1698 }
1699 return yuv;
1700}
1701
1702static std::vector<std::shared_ptr<Texture>> CreateTestYUVTextures(
1704 YUVColorSpace yuv_color_space) {
1705 Vector3 red = {244.0 / 255.0, 67.0 / 255.0, 54.0 / 255.0};
1706 Vector3 green = {76.0 / 255.0, 175.0 / 255.0, 80.0 / 255.0};
1707 Vector3 blue = {33.0 / 255.0, 150.0 / 255.0, 243.0 / 255.0};
1708 Vector3 white = {1.0, 1.0, 1.0};
1709 Vector3 red_yuv = RGBToYUV(red, yuv_color_space);
1710 Vector3 green_yuv = RGBToYUV(green, yuv_color_space);
1711 Vector3 blue_yuv = RGBToYUV(blue, yuv_color_space);
1712 Vector3 white_yuv = RGBToYUV(white, yuv_color_space);
1713 std::vector<Vector3> yuvs{red_yuv, green_yuv, blue_yuv, white_yuv};
1714 std::vector<uint8_t> y_data;
1715 std::vector<uint8_t> uv_data;
1716 for (int i = 0; i < 4; i++) {
1717 auto yuv = yuvs[i];
1718 uint8_t y = std::round(yuv.x * 255.0);
1719 uint8_t u = std::round(yuv.y * 255.0);
1720 uint8_t v = std::round(yuv.z * 255.0);
1721 for (int j = 0; j < 16; j++) {
1722 y_data.push_back(y);
1723 }
1724 for (int j = 0; j < 8; j++) {
1725 uv_data.push_back(j % 2 == 0 ? u : v);
1726 }
1727 }
1728 auto cmd_buffer = context->CreateCommandBuffer();
1729 auto blit_pass = cmd_buffer->CreateBlitPass();
1730
1731 impeller::TextureDescriptor y_texture_descriptor;
1733 y_texture_descriptor.format = PixelFormat::kR8UNormInt;
1734 y_texture_descriptor.size = {8, 8};
1735 auto y_texture =
1736 context->GetResourceAllocator()->CreateTexture(y_texture_descriptor);
1737 auto y_mapping = std::make_shared<fml::DataMapping>(y_data);
1738 auto y_mapping_buffer =
1739 context->GetResourceAllocator()->CreateBufferWithCopy(*y_mapping);
1740
1741 blit_pass->AddCopy(DeviceBuffer::AsBufferView(y_mapping_buffer), y_texture);
1742
1743 impeller::TextureDescriptor uv_texture_descriptor;
1744 uv_texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
1745 uv_texture_descriptor.format = PixelFormat::kR8G8UNormInt;
1746 uv_texture_descriptor.size = {4, 4};
1747 auto uv_texture =
1748 context->GetResourceAllocator()->CreateTexture(uv_texture_descriptor);
1749 auto uv_mapping = std::make_shared<fml::DataMapping>(uv_data);
1750 auto uv_mapping_buffer =
1751 context->GetResourceAllocator()->CreateBufferWithCopy(*uv_mapping);
1752
1753 blit_pass->AddCopy(DeviceBuffer::AsBufferView(uv_mapping_buffer), uv_texture);
1754
1755 if (!blit_pass->EncodeCommands() ||
1756 !context->GetCommandQueue()->Submit({cmd_buffer}).ok()) {
1757 FML_DLOG(ERROR) << "Could not copy contents into Y/UV texture.";
1758 }
1759
1760 return {y_texture, uv_texture};
1761}
1762
1763TEST_P(EntityTest, YUVToRGBFilter) {
1764 if (GetParam() != PlaygroundBackend::kMetal &&
1765 GetParam() != PlaygroundBackend::kMetalSDF) {
1766 // @see (114588) : Support YUV to RGB filter on OpenGLES backend.
1767 // The issue was closed as low priority as the support is only really
1768 // needed for iOS. Also, Vulkan isn't supported either.
1769 GTEST_SKIP() << "YUV to RGB filter is only supported on Metal backends.";
1770 }
1771
1772 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1773 YUVColorSpace yuv_color_space_array[2]{YUVColorSpace::kBT601FullRange,
1775 for (int i = 0; i < 2; i++) {
1776 auto yuv_color_space = yuv_color_space_array[i];
1777 auto textures =
1778 CreateTestYUVTextures(GetContext().get(), yuv_color_space);
1779 auto filter_contents = FilterContents::MakeYUVToRGBFilter(
1780 textures[0], textures[1], yuv_color_space);
1781 Entity filter_entity;
1782 filter_entity.SetContents(filter_contents);
1783 auto snapshot =
1784 filter_contents->RenderToSnapshot(context, filter_entity, {});
1785
1786 Entity entity;
1787 auto contents = TextureContents::MakeRect(Rect::MakeLTRB(0, 0, 256, 256));
1788 contents->SetTexture(snapshot->texture);
1789 contents->SetSourceRect(Rect::MakeSize(snapshot->texture->GetSize()));
1790 entity.SetContents(contents);
1791 entity.SetTransform(
1792 Matrix::MakeTranslation({static_cast<Scalar>(100 + 400 * i), 300}));
1793 entity.Render(context, pass);
1794 }
1795 return true;
1796 };
1797 ASSERT_TRUE(OpenPlaygroundHere(callback));
1798}
1799
1800TEST_P(EntityTest, RuntimeEffect) {
1801 auto runtime_stages_result =
1802 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1803 ABSL_ASSERT_OK(runtime_stages_result);
1804 std::shared_ptr<RuntimeStage> runtime_stage =
1805 runtime_stages_result.value()[GetRuntimeStageBackend()];
1806 ASSERT_TRUE(runtime_stage);
1807 ASSERT_TRUE(runtime_stage->IsDirty());
1808
1809 bool expect_dirty = true;
1810
1811 PipelineRef first_pipeline;
1812 std::unique_ptr<Geometry> geom = Geometry::MakeCover();
1813
1814 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1815 EXPECT_EQ(runtime_stage->IsDirty(), expect_dirty);
1816
1817 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1818 contents->SetRuntimeStage(runtime_stage);
1819
1820 struct FragUniforms {
1821 Vector2 iResolution;
1822 Scalar iTime;
1823 } frag_uniforms = {
1824 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1825 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1826 };
1827 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1828 uniform_data->resize(sizeof(FragUniforms));
1829 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1830 contents->SetUniformData(uniform_data);
1831
1832 Entity entity;
1833 entity.SetContents(contents);
1834 bool result = contents->Render(context, entity, pass);
1835
1836 if (expect_dirty) {
1837 first_pipeline = pass.GetCommands().back().pipeline;
1838 } else {
1839 EXPECT_EQ(pass.GetCommands().back().pipeline, first_pipeline);
1840 }
1841 expect_dirty = false;
1842 return result;
1843 };
1844
1845 // Simulate some renders and hot reloading of the shader.
1846 ContentContext& content_context = GetContentContext();
1847 {
1849 content_context.GetRenderTargetCache()->CreateOffscreen(
1850 *content_context.GetContext(), {1, 1}, 1u);
1851
1852 testing::MockRenderPass mock_pass(GetContext(), target);
1853 callback(content_context, mock_pass);
1854 callback(content_context, mock_pass);
1855
1856 // Dirty the runtime stage.
1857 auto runtime_stages_result =
1858 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1859 ABSL_ASSERT_OK(runtime_stages_result);
1860 runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1861
1862 ASSERT_TRUE(runtime_stage->IsDirty());
1863 expect_dirty = true;
1864
1865 callback(content_context, mock_pass);
1866 }
1867}
1868
1869TEST_P(EntityTest, RuntimeEffectCanSuccessfullyRender) {
1870 auto runtime_stages_result =
1871 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1872 ABSL_ASSERT_OK(runtime_stages_result);
1873 auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1874 ASSERT_TRUE(runtime_stage);
1875 ASSERT_TRUE(runtime_stage->IsDirty());
1876
1877 auto geom = Geometry::MakeCover();
1878 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1879 contents->SetRuntimeStage(runtime_stage);
1880
1881 struct FragUniforms {
1882 Vector2 iResolution;
1883 Scalar iTime;
1884 } frag_uniforms = {
1885 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1886 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1887 };
1888 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1889 uniform_data->resize(sizeof(FragUniforms));
1890 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1891 contents->SetUniformData(uniform_data);
1892
1893 Entity entity;
1894 entity.SetContents(contents);
1895
1896 // Create a render target with a depth-stencil, similar to how EntityPass
1897 // does.
1899 GetContentContext().GetRenderTargetCache()->CreateOffscreenMSAA(
1900 *GetContext(), {GetWindowSize().width, GetWindowSize().height}, 1,
1901 "RuntimeEffect Texture");
1902 testing::MockRenderPass pass(GetContext(), target);
1903
1904 ASSERT_TRUE(contents->Render(GetContentContext(), entity, pass));
1905 ASSERT_EQ(pass.GetCommands().size(), 1u);
1906 const auto& command = pass.GetCommands()[0];
1907 ASSERT_TRUE(command.pipeline->GetDescriptor()
1908 .GetDepthStencilAttachmentDescriptor()
1909 .has_value());
1910 ASSERT_TRUE(command.pipeline->GetDescriptor()
1911 .GetFrontStencilAttachmentDescriptor()
1912 .has_value());
1913}
1914
1915TEST_P(EntityTest, RuntimeEffectCanPrecache) {
1916 auto runtime_stages_result =
1917 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1918 ABSL_ASSERT_OK(runtime_stages_result);
1919 std::shared_ptr<RuntimeStage> runtime_stage =
1920 runtime_stages_result.value()[GetRuntimeStageBackend()];
1921 ASSERT_TRUE(runtime_stage);
1922 ASSERT_TRUE(runtime_stage->IsDirty());
1923
1924 auto geom = Geometry::MakeCover();
1925 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1926 contents->SetRuntimeStage(runtime_stage);
1927
1928 EXPECT_TRUE(contents->BootstrapShader(GetContentContext()));
1929}
1930
1931TEST_P(EntityTest, RuntimeEffectSetsRightSizeWhenUniformIsStruct) {
1932 if (GetBackend() != PlaygroundBackend::kVulkan) {
1933 GTEST_SKIP() << "Test only applies to Vulkan";
1934 }
1935
1936 auto runtime_stages_result =
1937 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1938 ABSL_ASSERT_OK(runtime_stages_result);
1939 auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1940 ASSERT_TRUE(runtime_stage);
1941 ASSERT_TRUE(runtime_stage->IsDirty());
1942
1943 auto geom = Geometry::MakeCover();
1944 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1945 contents->SetRuntimeStage(runtime_stage);
1946
1947 struct FragUniforms {
1948 Vector2 iResolution;
1949 Scalar iTime;
1950 } frag_uniforms = {
1951 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1952 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1953 };
1954 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1955 uniform_data->resize(sizeof(FragUniforms));
1956 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1957
1958 auto buffer_view = RuntimeEffectContents::EmplaceUniform(
1959 uniform_data->data(), GetContentContext().GetTransientsDataBuffer(),
1960 runtime_stage->GetUniforms()[0]);
1961
1962 // 16 bytes:
1963 // 8 bytes for iResolution
1964 // 4 bytes for iTime
1965 // 4 bytes padding
1966 EXPECT_EQ(buffer_view.GetRange().length, 16u);
1967}
1968
1969TEST_P(EntityTest, ColorFilterWithForegroundColorAdvancedBlend) {
1970 auto image = CreateTextureForFixture("boston.jpg");
1971 auto filter = ColorFilterContents::MakeBlend(
1973
1974 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1975 Entity entity;
1976 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1977 Matrix::MakeTranslation({500, 300}) *
1978 Matrix::MakeScale(Vector2{0.5, 0.5}));
1979 entity.SetContents(filter);
1980 return entity.Render(context, pass);
1981 };
1982 ASSERT_TRUE(OpenPlaygroundHere(callback));
1983}
1984
1985TEST_P(EntityTest, ColorFilterWithForegroundColorClearBlend) {
1986 auto image = CreateTextureForFixture("boston.jpg");
1987 auto filter = ColorFilterContents::MakeBlend(
1989
1990 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1991 Entity entity;
1992 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1993 Matrix::MakeTranslation({500, 300}) *
1994 Matrix::MakeScale(Vector2{0.5, 0.5}));
1995 entity.SetContents(filter);
1996 return entity.Render(context, pass);
1997 };
1998 ASSERT_TRUE(OpenPlaygroundHere(callback));
1999}
2000
2001TEST_P(EntityTest, ColorFilterWithForegroundColorSrcBlend) {
2002 auto image = CreateTextureForFixture("boston.jpg");
2003 auto filter = ColorFilterContents::MakeBlend(
2005
2006 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2007 Entity entity;
2008 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2009 Matrix::MakeTranslation({500, 300}) *
2010 Matrix::MakeScale(Vector2{0.5, 0.5}));
2011 entity.SetContents(filter);
2012 return entity.Render(context, pass);
2013 };
2014 ASSERT_TRUE(OpenPlaygroundHere(callback));
2015}
2016
2017TEST_P(EntityTest, ColorFilterWithForegroundColorDstBlend) {
2018 auto image = CreateTextureForFixture("boston.jpg");
2019 auto filter = ColorFilterContents::MakeBlend(
2021
2022 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2023 Entity entity;
2024 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2025 Matrix::MakeTranslation({500, 300}) *
2026 Matrix::MakeScale(Vector2{0.5, 0.5}));
2027 entity.SetContents(filter);
2028 return entity.Render(context, pass);
2029 };
2030 ASSERT_TRUE(OpenPlaygroundHere(callback));
2031}
2032
2033TEST_P(EntityTest, ColorFilterWithForegroundColorSrcInBlend) {
2034 auto image = CreateTextureForFixture("boston.jpg");
2035 auto filter = ColorFilterContents::MakeBlend(
2037
2038 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2039 Entity entity;
2040 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2041 Matrix::MakeTranslation({500, 300}) *
2042 Matrix::MakeScale(Vector2{0.5, 0.5}));
2043 entity.SetContents(filter);
2044 return entity.Render(context, pass);
2045 };
2046 ASSERT_TRUE(OpenPlaygroundHere(callback));
2047}
2048
2049TEST_P(EntityTest, CoverageForStrokePathWithNegativeValuesInTransform) {
2050 auto arrow_head = flutter::DlPathBuilder{}
2051 .MoveTo({50, 120})
2052 .LineTo({120, 190})
2053 .LineTo({190, 120})
2054 .TakePath();
2055 auto geometry = Geometry::MakeStrokePath(arrow_head, //
2056 {
2057 .width = 15.0f,
2058 .cap = Cap::kRound,
2059 .join = Join::kRound,
2060 .miter_limit = 4.0f,
2061 });
2062
2063 auto transform = Matrix::MakeTranslation({300, 300}) *
2065 // Note that e[0][0] used to be tested here, but it was -epsilon solely
2066 // due to floating point inaccuracy in the transcendental trig functions.
2067 // e[1][0] is the intended negative value that we care about (-1.0) as it
2068 // comes from the rotation of pi/2.
2069 EXPECT_LT(transform.e[1][0], 0.0f);
2070 auto coverage = geometry->GetCoverage(transform);
2071 ASSERT_RECT_NEAR(coverage.value(), Rect::MakeXYWH(102.5, 342.5, 85, 155));
2072}
2073
2074TEST_P(EntityTest, SolidColorContentsIsOpaque) {
2075 Matrix matrix;
2076 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2077 SolidColorContents contents(geom.get());
2078
2079 contents.SetColor(Color::CornflowerBlue());
2080 EXPECT_TRUE(contents.IsOpaque(matrix));
2081 contents.SetColor(Color::CornflowerBlue().WithAlpha(0.5));
2082 EXPECT_FALSE(contents.IsOpaque(matrix));
2083
2084 // Create stroked path that required alpha coverage.
2085 auto geom2 = Geometry::MakeStrokePath(
2086 flutter::DlPath::MakeLine({0, 0}, {100, 100}), {.width = 0.05});
2087 SolidColorContents contents2(geom2.get());
2088 contents2.SetColor(Color::CornflowerBlue());
2089
2090 EXPECT_FALSE(contents2.IsOpaque(matrix));
2091}
2092
2093TEST_P(EntityTest, ConicalGradientContentsIsOpaque) {
2094 Matrix matrix;
2095 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2096 ConicalGradientContents contents(geom.get());
2097
2098 contents.SetColors({Color::CornflowerBlue()});
2099 EXPECT_FALSE(contents.IsOpaque(matrix));
2100 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2101 EXPECT_FALSE(contents.IsOpaque(matrix));
2102
2103 // Create stroked path that required alpha coverage.
2104 auto geom2 = Geometry::MakeStrokePath(
2105 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2106 {.width = 0.05f});
2107 ConicalGradientContents contents2(geom2.get());
2108 contents2.SetColors({Color::CornflowerBlue()});
2109
2110 EXPECT_FALSE(contents2.IsOpaque(matrix));
2111}
2112
2113TEST_P(EntityTest, LinearGradientContentsIsOpaque) {
2114 Matrix matrix;
2115 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2116 LinearGradientContents contents(geom.get());
2117
2118 contents.SetColors({Color::CornflowerBlue()});
2119 EXPECT_TRUE(contents.IsOpaque(matrix));
2120 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2121 EXPECT_FALSE(contents.IsOpaque(matrix));
2122 contents.SetColors({Color::CornflowerBlue()});
2124 EXPECT_FALSE(contents.IsOpaque(matrix));
2125
2126 // Create stroked path that required alpha coverage.
2127 auto geom2 = Geometry::MakeStrokePath(
2128 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2129 {.width = 0.05f});
2130 LinearGradientContents contents2(geom2.get());
2131 contents2.SetColors({Color::CornflowerBlue()});
2132
2133 EXPECT_FALSE(contents2.IsOpaque(matrix));
2134}
2135
2136TEST_P(EntityTest, RadialGradientContentsIsOpaque) {
2137 Matrix matrix;
2138 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2139 RadialGradientContents contents(geom.get());
2140
2141 contents.SetColors({Color::CornflowerBlue()});
2142 EXPECT_TRUE(contents.IsOpaque(matrix));
2143 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2144 EXPECT_FALSE(contents.IsOpaque(matrix));
2145 contents.SetColors({Color::CornflowerBlue()});
2147 EXPECT_FALSE(contents.IsOpaque(matrix));
2148
2149 // Create stroked path that required alpha coverage.
2150 auto geom2 = Geometry::MakeStrokePath(
2151 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2152 {.width = 0.05});
2153 RadialGradientContents contents2(geom2.get());
2154 contents2.SetColors({Color::CornflowerBlue()});
2155
2156 EXPECT_FALSE(contents2.IsOpaque(matrix));
2157}
2158
2159TEST_P(EntityTest, SweepGradientContentsIsOpaque) {
2160 Matrix matrix;
2161 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2162 SweepGradientContents contents(geom.get());
2163
2164 contents.SetColors({Color::CornflowerBlue()});
2165 EXPECT_TRUE(contents.IsOpaque(matrix));
2166 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2167 EXPECT_FALSE(contents.IsOpaque(matrix));
2168 contents.SetColors({Color::CornflowerBlue()});
2170 EXPECT_FALSE(contents.IsOpaque(matrix));
2171
2172 // Create stroked path that required alpha coverage.
2173 auto geom2 = Geometry::MakeStrokePath(
2174 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2175 {.width = 0.05f});
2176 SweepGradientContents contents2(geom2.get());
2177 contents2.SetColors({Color::CornflowerBlue()});
2178
2179 EXPECT_FALSE(contents2.IsOpaque(matrix));
2180}
2181
2182TEST_P(EntityTest, TiledTextureContentsIsOpaque) {
2183 Matrix matrix;
2184 auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg");
2185 auto geom = Geometry::MakeCover();
2186 TiledTextureContents contents(geom.get());
2187 contents.SetTexture(bay_bridge);
2188 // This is a placeholder test. Images currently never decompress as opaque
2189 // (whether in Flutter or the playground), and so this should currently always
2190 // return false in practice.
2191 EXPECT_FALSE(contents.IsOpaque(matrix));
2192}
2193
2194TEST_P(EntityTest, PointFieldGeometryCoverage) {
2195 std::vector<Point> points = {{10, 20}, {100, 200}};
2196 PointFieldGeometry geometry(points.data(), 2, 5.0, false);
2197 ASSERT_EQ(geometry.GetCoverage(Matrix()), Rect::MakeLTRB(5, 15, 105, 205));
2198 ASSERT_EQ(geometry.GetCoverage(Matrix::MakeTranslation({30, 0, 0})),
2199 Rect::MakeLTRB(35, 15, 135, 205));
2200}
2201
2202TEST_P(EntityTest, ColorFilterContentsWithLargeGeometry) {
2203 Entity entity;
2204 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2205 auto src_geom = Geometry::MakeRect(Rect::MakeLTRB(-300, -500, 30000, 50000));
2206 auto src_contents = std::make_shared<SolidColorContents>(src_geom.get());
2207 src_contents->SetColor(Color::Red());
2208
2209 auto dst_geom = Geometry::MakeRect(Rect::MakeLTRB(300, 500, 20000, 30000));
2210 auto dst_contents = std::make_shared<SolidColorContents>(dst_geom.get());
2211 dst_contents->SetColor(Color::Blue());
2212
2213 auto contents = ColorFilterContents::MakeBlend(
2214 BlendMode::kSrcOver, {FilterInput::Make(dst_contents, false),
2215 FilterInput::Make(src_contents, false)});
2216 entity.SetContents(std::move(contents));
2217 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2218}
2219
2220TEST_P(EntityTest, TextContentsCeilsGlyphScaleToDecimal) {
2221 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.4321111f), Rational(43, 100));
2222 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.5321111f), Rational(53, 100));
2223 ASSERT_EQ(TextFrame::RoundScaledFontSize(2.1f), Rational(21, 10));
2224 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.0f), Rational(0, 1));
2225 ASSERT_EQ(TextFrame::RoundScaledFontSize(100000000.0f), Rational(48, 1));
2226}
2227
2228TEST_P(EntityTest, SpecializationConstantsAreAppliedToVariants) {
2229 ContentContext& content_context = GetContentContext();
2230
2231 auto default_gyph = content_context.GetGlyphAtlasPipeline({
2232 .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2233 .has_depth_stencil_attachments = false,
2234 });
2235 auto alt_gyph = content_context.GetGlyphAtlasPipeline(
2236 {.color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2237 .has_depth_stencil_attachments = true});
2238
2239 EXPECT_NE(default_gyph, alt_gyph);
2240 EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2241 alt_gyph->GetDescriptor().GetSpecializationConstants());
2242
2243 auto use_a8 = GetContext()->GetCapabilities()->GetDefaultGlyphAtlasFormat() ==
2245
2246 std::vector<Scalar> expected_constants = {static_cast<Scalar>(use_a8)};
2247 EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2248 expected_constants);
2249}
2250
2251TEST_P(EntityTest, DecalSpecializationAppliedToMorphologyFilter) {
2252 ContentContext& content_context = GetContentContext();
2253 auto default_color_burn = content_context.GetMorphologyFilterPipeline({
2254 .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2255 });
2256
2257 auto decal_supported = static_cast<Scalar>(
2258 GetContext()->GetCapabilities()->SupportsDecalSamplerAddressMode());
2259 std::vector<Scalar> expected_constants = {decal_supported};
2260 ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(),
2261 expected_constants);
2262}
2263
2264// This doesn't really tell you if the hashes will have frequent
2265// collisions, but since this type is only used to hash a bounded
2266// set of options, we can just compare benchmarks.
2267TEST_P(EntityTest, ContentContextOptionsHasReasonableHashFunctions) {
2269 auto hash_a = opts.ToKey();
2270
2272 auto hash_b = opts.ToKey();
2273
2274 opts.has_depth_stencil_attachments = false;
2275 auto hash_c = opts.ToKey();
2276
2278 auto hash_d = opts.ToKey();
2279
2280 EXPECT_NE(hash_a, hash_b);
2281 EXPECT_NE(hash_b, hash_c);
2282 EXPECT_NE(hash_c, hash_d);
2283}
2284
2285#ifdef FML_OS_LINUX
2286TEST_P(EntityTest, FramebufferFetchVulkanBindingOffsetIsTheSame) {
2287 // Using framebuffer fetch on Vulkan requires that we maintain a subpass input
2288 // binding that we don't have a good route for configuring with the
2289 // current metadata approach. This test verifies that the binding value
2290 // doesn't change
2291 // from the expected constant.
2292 // See also:
2293 // * impeller/renderer/backend/vulkan/binding_helpers_vk.cc
2294 // * impeller/entity/shaders/blending/framebuffer_blend.frag
2295 // This test only works on Linux because macOS hosts incorrectly
2296 // populate the
2297 // Vulkan descriptor sets based on the MSL compiler settings.
2298
2299 bool expected_layout = false;
2301 FragmentShader::kDescriptorSetLayouts) {
2302 if (layout.binding == 64 &&
2303 layout.descriptor_type == DescriptorType::kInputAttachment) {
2304 expected_layout = true;
2305 }
2306 }
2307 EXPECT_TRUE(expected_layout);
2308}
2309#endif
2310
2311TEST_P(EntityTest, FillPathGeometryGetPositionBufferReturnsExpectedMode) {
2313 testing::MockRenderPass mock_pass(GetContext(), target);
2314
2315 auto get_result = [this, &mock_pass](const flutter::DlPath& path) {
2316 auto geometry = Geometry::MakeFillPath(
2317 path, /* inner rect */ Rect::MakeLTRB(0, 0, 100, 100));
2318 return geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2319 };
2320
2321 // Convex path
2322 {
2323 GeometryResult result =
2324 get_result(flutter::DlPath::MakeRect(Rect::MakeLTRB(0, 0, 100, 100)));
2325 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal);
2326 }
2327
2328 // Concave path
2329 {
2331 .MoveTo({0, 0})
2332 .LineTo({100, 0})
2333 .LineTo({100, 100})
2334 .LineTo({51, 50})
2335 .Close()
2336 .TakePath();
2337 GeometryResult result = get_result(path);
2338 EXPECT_EQ(result.mode, GeometryResult::Mode::kNonZero);
2339 }
2340}
2341
2342TEST_P(EntityTest, StrokeArcGeometryGetPositionBufferReturnsExpectedMode) {
2344 testing::MockRenderPass mock_pass(GetContext(), target);
2345 Rect oval_bounds = Rect::MakeLTRB(100, 100, 200, 200);
2346
2347 // Butt caps never overlap
2348 {
2349 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kButt};
2350 for (auto start = 0; start < 360; start += 60) {
2351 for (auto sweep = 0; sweep < 360; sweep += 12) {
2352 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2353 Degrees(sweep), stroke);
2354
2355 GeometryResult result =
2356 geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2357
2358 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2359 << "start: " << start << " sweep: " << sweep;
2360 }
2361 }
2362 }
2363
2364 // Round caps with 10 stroke width overlap starting at 348.6 degrees
2365 {
2366 StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kRound};
2367 for (auto start = 0; start < 360; start += 60) {
2368 for (auto sweep = 0; sweep < 360; sweep += 12) {
2369 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2370 Degrees(sweep), stroke);
2371
2372 GeometryResult result =
2373 geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2374
2375 if (sweep < 348.6) {
2376 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2377 << "start: " << start << " sweep: " << sweep;
2378 } else {
2380 << "start: " << start << " sweep: " << sweep;
2381 }
2382 }
2383 }
2384 }
2385
2386 // Round caps with 50 stroke width overlap starting at 300.1 degrees
2387 {
2388 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kRound};
2389 for (auto start = 0; start < 360; start += 60) {
2390 for (auto sweep = 0; sweep < 360; sweep += 12) {
2391 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2392 Degrees(sweep), stroke);
2393
2394 GeometryResult result =
2395 geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2396
2397 if (sweep < 300.0) {
2398 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2399 << "start: " << start << " sweep: " << sweep;
2400 } else {
2402 << "start: " << start << " sweep: " << sweep;
2403 }
2404 }
2405 }
2406 }
2407
2408 // Square caps with 10 stroke width overlap starting at 347.4 degrees
2409 {
2410 StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kSquare};
2411 for (auto start = 0; start < 360; start += 60) {
2412 for (auto sweep = 0; sweep < 360; sweep += 12) {
2413 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2414 Degrees(sweep), stroke);
2415
2416 GeometryResult result =
2417 geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2418
2419 if (sweep < 347.4) {
2420 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2421 << "start: " << start << " sweep: " << sweep;
2422 } else {
2424 << "start: " << start << " sweep: " << sweep;
2425 }
2426 }
2427 }
2428 }
2429
2430 // Square caps with 50 stroke width overlap starting at 270.1 degrees
2431 {
2432 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kSquare};
2433 for (auto start = 0; start < 360; start += 60) {
2434 for (auto sweep = 0; sweep < 360; sweep += 12) {
2435 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2436 Degrees(sweep), stroke);
2437
2438 GeometryResult result =
2439 geometry->GetPositionBuffer(GetContentContext(), {}, mock_pass);
2440
2441 if (sweep < 270.1) {
2442 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2443 << "start: " << start << " sweep: " << sweep;
2444 } else {
2446 << "start: " << start << " sweep: " << sweep;
2447 }
2448 }
2449 }
2450 }
2451}
2452
2453TEST_P(EntityTest, FailOnValidationError) {
2454 if (GetParam() != PlaygroundBackend::kVulkan) {
2455 GTEST_SKIP() << "Validation is only fatal on Vulkan backend.";
2456 }
2457 EXPECT_DEATH(
2458 // The easiest way to trigger a validation error is to try to compile
2459 // a shader with an unsupported pixel format.
2460 GetContentContext().GetBlendColorBurnPipeline({
2461 .color_attachment_pixel_format = PixelFormat::kUnknown,
2462 .has_depth_stencil_attachments = false,
2463 }),
2464 "");
2465}
2466
2467TEST_P(EntityTest, CanComputeGeometryForEmptyPathsWithoutCrashing) {
2469
2470 EXPECT_TRUE(path.GetBounds().IsEmpty());
2471
2472 auto geom = Geometry::MakeFillPath(path);
2473
2474 Entity entity;
2476 GetContentContext().GetRenderTargetCache()->CreateOffscreen(*GetContext(),
2477 {1, 1}, 1u);
2479 auto position_result =
2480 geom->GetPositionBuffer(GetContentContext(), entity, render_pass);
2481
2482 EXPECT_EQ(position_result.vertex_buffer.vertex_count, 0u);
2483
2484 EXPECT_EQ(geom->GetResultMode(), GeometryResult::Mode::kNormal);
2485}
2486
2487TEST_P(EntityTest, CanRenderEmptyPathsWithoutCrashing) {
2489
2490 EXPECT_TRUE(path.GetBounds().IsEmpty());
2491
2492 std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
2493 auto contents = std::make_shared<SolidColorContents>(geom.get());
2494 contents->SetColor(Color::Red());
2495
2496 Entity entity;
2497 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2498 entity.SetContents(contents);
2499
2500 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2501}
2502
2503TEST_P(EntityTest, DrawSuperEllipse) {
2504 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2505 // UI state.
2506 static float alpha = 10;
2507 static float beta = 10;
2508 static float radius = 40;
2509 static int degree = 4;
2510 static Color color = Color::Red();
2511
2512 if (IsPlaygroundEnabled()) {
2513 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2514 ImGui::SliderFloat("Alpha", &alpha, 0, 100);
2515 ImGui::SliderFloat("Beta", &beta, 0, 100);
2516 ImGui::SliderInt("Degreee", &degree, 1, 20);
2517 ImGui::SliderFloat("Radius", &radius, 0, 400);
2518 ImGui::ColorEdit4("Color", reinterpret_cast<float*>(&color));
2519 ImGui::End();
2520 }
2521
2522 std::unique_ptr<SuperellipseGeometry> geom =
2523 std::make_unique<SuperellipseGeometry>(Point{400, 400}, radius, degree,
2524 alpha, beta);
2525 auto contents = std::make_shared<SolidColorContents>(geom.get());
2526 contents->SetColor(color);
2527
2528 Entity entity;
2529 entity.SetContents(contents);
2530
2531 return entity.Render(context, pass);
2532 };
2533
2534 ASSERT_TRUE(OpenPlaygroundHere(callback));
2535}
2536
2537TEST_P(EntityTest, DrawRoundSuperEllipse) {
2538 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2539 // UI state.
2540 static int style_index = 0;
2541 static Point center = {830, 830};
2542 static Point size = {600, 600};
2543 static bool horizontal_symmetry = true;
2544 static bool vertical_symmetry = true;
2545 static bool corner_symmetry = true;
2546
2547 const char* style_options[] = {"Fill", "Stroke"};
2548
2549 // Initially radius_tl[0] will be mirrored to all 8 values since all 3
2550 // symmetries are enabled.
2551 static std::array<float, 2> radius_tl = {200};
2552 static std::array<float, 2> radius_tr;
2553 static std::array<float, 2> radius_bl;
2554 static std::array<float, 2> radius_br;
2555
2556 auto AddRadiusControl = [](std::array<float, 2>& radii, const char* tb_name,
2557 const char* lr_name) {
2558 std::string name = "Radius";
2559 if (!horizontal_symmetry || !vertical_symmetry) {
2560 name += ":";
2561 }
2562 if (!vertical_symmetry) {
2563 name = name + " " + tb_name;
2564 }
2565 if (!horizontal_symmetry) {
2566 name = name + " " + lr_name;
2567 }
2568 if (corner_symmetry) {
2569 ImGui::SliderFloat(name.c_str(), radii.data(), 0, 1000);
2570 } else {
2571 ImGui::SliderFloat2(name.c_str(), radii.data(), 0, 1000);
2572 }
2573 };
2574
2575 if (corner_symmetry) {
2576 radius_tl[1] = radius_tl[0];
2577 radius_tr[1] = radius_tr[0];
2578 radius_bl[1] = radius_bl[0];
2579 radius_br[1] = radius_br[0];
2580 }
2581
2582 if (IsPlaygroundEnabled()) {
2583 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2584 ImGui::Combo("Style", &style_index, style_options,
2585 sizeof(style_options) / sizeof(char*));
2586 ImGui::SliderFloat2("Center", &center.x, 0, 1000);
2587 ImGui::SliderFloat2("Size", &size.x, 0, 1000);
2588 ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry);
2589 ImGui::Checkbox("Symmetry: Vertical", &vertical_symmetry);
2590 ImGui::Checkbox("Symmetry: Corners", &corner_symmetry);
2591 AddRadiusControl(radius_tl, "Top", "Left");
2592 if (!horizontal_symmetry) {
2593 AddRadiusControl(radius_tr, "Top", "Right");
2594 } else {
2595 radius_tr = radius_tl;
2596 }
2597 if (!vertical_symmetry) {
2598 AddRadiusControl(radius_bl, "Bottom", "Left");
2599 } else {
2600 radius_bl = radius_tl;
2601 }
2602 if (!horizontal_symmetry && !vertical_symmetry) {
2603 AddRadiusControl(radius_br, "Bottom", "Right");
2604 } else {
2605 if (horizontal_symmetry) {
2606 radius_br = radius_bl;
2607 } else {
2608 radius_br = radius_tr;
2609 }
2610 }
2611 ImGui::End();
2612 }
2613
2614 RoundingRadii radii{
2615 .top_left = {radius_tl[0], radius_tl[1]},
2616 .top_right = {radius_tr[0], radius_tr[1]},
2617 .bottom_left = {radius_bl[0], radius_bl[1]},
2618 .bottom_right = {radius_br[0], radius_br[1]},
2619 };
2620
2622 Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2623
2625 std::unique_ptr<Geometry> geom;
2626 if (style_index == 0) {
2627 geom = std::make_unique<RoundSuperellipseGeometry>(
2628 Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2629 } else {
2631 geom = Geometry::MakeStrokePath(path, {.width = 2.0f});
2632 }
2633
2634 auto contents = std::make_shared<SolidColorContents>(geom.get());
2635 contents->SetColor(Color::Red());
2636
2637 Entity entity;
2638 entity.SetContents(contents);
2639
2640 return entity.Render(context, pass);
2641 };
2642
2643 ASSERT_TRUE(OpenPlaygroundHere(callback));
2644}
2645
2646TEST_P(EntityTest, DrawRoundSuperEllipseWithLargeN) {
2647 // This playground shows the enlarged corner of a rounded superellipse to
2648 // compare pathing algorithm against the filling algorithm (benchmark) at very
2649 // large ratio.
2650 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2651 constexpr float corner_radius = 100.0;
2652
2653 // UI state.
2654 static float logarithm_of_ratio = 1.5; // ratio = size / corner_radius
2655
2656 float ratio = std::exp(logarithm_of_ratio);
2657
2658 float rect_size = corner_radius * ratio;
2659 Rect rect = Rect::MakeLTRB(0, 0, rect_size, rect_size);
2660 constexpr float screen_canvas_padding = 200.0f;
2661 constexpr float screen_canvas_size = 1000.0f;
2662
2663 // Scale so that "corner radius" is as long as half the canvas.
2664 float scale = screen_canvas_size / 2 / corner_radius;
2665
2666 if (IsPlaygroundEnabled()) {
2667 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2668 ImGui::SliderFloat("log(Ratio)", &logarithm_of_ratio, 1.0, 8.0);
2669 ImGui::LabelText("Ratio", "%.2g", static_cast<double>(ratio));
2670 ImGui::Text(" where Ratio = RectSize / CornerRadius");
2671 ImGui::End();
2672 }
2673
2674 auto top_right = Vector2(screen_canvas_size * 1.3f, screen_canvas_padding);
2675 auto transform = Matrix::MakeTranslation(top_right) *
2676 Matrix::MakeScale(Vector2(scale, scale)) *
2677 Matrix::MakeTranslation(Vector2(-rect_size, 0));
2678 bool success = true;
2679
2680 auto fill_geom =
2681 std::make_unique<RoundSuperellipseGeometry>(rect, corner_radius);
2682 // Fill
2683 {
2684 auto contents = std::make_shared<SolidColorContents>(fill_geom.get());
2685 contents->SetColor(Color::Red());
2686
2687 Entity entity;
2688 entity.SetContents(contents);
2689 entity.SetTransform(transform);
2690
2691 success = success && entity.Render(context, pass);
2692 }
2693
2694 // Stroke
2696 RoundSuperellipse::MakeRectRadius(rect, corner_radius));
2697 auto stroke_geom = Geometry::MakeStrokePath(path, {.width = 2 / scale});
2698 {
2699 auto contents = std::make_shared<SolidColorContents>(stroke_geom.get());
2700 contents->SetColor(Color::Blue());
2701
2702 Entity entity;
2703 entity.SetContents(contents);
2704 entity.SetTransform(transform);
2705
2706 success = success && entity.Render(context, pass);
2707 }
2708
2709 // Draw a ruler to show the length in portion of rect size.
2710 auto screen_top_right =
2711 Matrix::MakeScale(GetContentScale()).Invert() * top_right;
2712 constexpr float font_size = 13.0f;
2713 for (int i = -1; i < 100; i++) {
2714 float screen_offset_y = static_cast<float>(i) * 20.0f;
2715 std::string label;
2716 if (i == -1) {
2717 label = "Ruler: (in portion of rect size)";
2718 } else if (i == 0) {
2719 label = "- 0.0";
2720 } else {
2721 float portion_of_rect =
2722 screen_offset_y * GetContentScale().y / scale / rect_size;
2723 label = std::format("- {:.2g}", portion_of_rect);
2724 }
2725 if (IsPlaygroundEnabled()) {
2726 ImGui::GetBackgroundDrawList()->AddText(
2727 nullptr, font_size,
2728 // Draw the ruler at around the flat part of the curve, which is
2729 // somewhere to the left of the top right corner.
2730 //
2731 // Offset vertically by font_size/2 so that the hyphen aligns with
2732 // the top of shape.
2733 {screen_top_right.x - 500,
2734 screen_top_right.y + screen_offset_y - font_size / 2},
2735 IM_COL32_WHITE, label.c_str());
2736 }
2737 }
2738 return success;
2739 };
2740
2741 ASSERT_TRUE(OpenPlaygroundHere(callback));
2742}
2743
2744TEST_P(EntityTest, CanDrawRoundSuperEllipseWithTinyRadius) {
2745 // Regression test for https://github.com/flutter/flutter/issues/176894
2746 // Verify that a radius marginally below the minimum threshold can be
2747 // processed safely. The expectation is that the rounded corners degenerate
2748 // into sharp corners (four corner points) and that no NaNs or crashes occur.
2750 Rect::MakeLTRB(200, 200, 300, 300), 0.5 * kEhCloseEnough);
2751
2752 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2753 Entity entity;
2754
2755 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2756
2758 content_context.GetContext()->GetResourceAllocator());
2759
2760 auto render_target = allocator.CreateOffscreen(
2761 *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2762 auto pass = cmd_buffer->CreateRenderPass(render_target);
2763
2764 GeometryResult result =
2765 geom->GetPositionBuffer(content_context, entity, *pass);
2766
2767 EXPECT_EQ(result.vertex_buffer.vertex_count, 4u);
2768 Point* written_data = reinterpret_cast<Point*>(
2771
2772 std::vector<Point> expected = {Point(300.0, 200.0), Point(300.0, 300.0),
2773 Point(200.0, 200.0), Point(200.0, 300.0)};
2774
2775 for (size_t i = 0; i < expected.size(); i++) {
2776 const Point& point = written_data[i];
2777 EXPECT_NEAR(point.x, expected[i].x, 0.1);
2778 EXPECT_NEAR(point.y, expected[i].y, 0.1);
2779 }
2780}
2781
2782TEST_P(EntityTest, CanDrawRoundSuperEllipseWithJustEnoughRadius) {
2783 // Regression test for https://github.com/flutter/flutter/issues/176894
2784 // Verify that a radius marginally above the minimum threshold can be
2785 // processed safely. The expectation is that the rounded corners are
2786 // drawn as rounded and that no NaNs or crashes occur.
2788 Rect::MakeLTRB(200, 200, 300, 300), 1.1 * kEhCloseEnough);
2789
2790 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2791 Entity entity;
2792
2793 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2794
2796 content_context.GetContext()->GetResourceAllocator());
2797
2798 auto render_target = allocator.CreateOffscreen(
2799 *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2800 auto pass = cmd_buffer->CreateRenderPass(render_target);
2801
2802 GeometryResult result =
2803 geom->GetPositionBuffer(content_context, entity, *pass);
2804
2805 EXPECT_EQ(result.vertex_buffer.vertex_count, 200u);
2806 Point* written_data = reinterpret_cast<Point*>(
2809
2810 std::vector<Point> expected_head = {Point(250.0, 200.0), Point(299.9, 200.0),
2811 Point(200.1, 200.0), Point(299.9, 200.0)};
2812
2813 for (size_t i = 0; i < expected_head.size(); i++) {
2814 const Point& point = written_data[i];
2815 EXPECT_NEAR(point.x, expected_head[i].x, 0.1);
2816 EXPECT_NEAR(point.y, expected_head[i].y, 0.1);
2817 }
2818}
2819
2820TEST_P(EntityTest, SolidColorApplyColorFilter) {
2821 auto geom = Geometry::MakeCover();
2822 auto contents = SolidColorContents(geom.get());
2823 contents.SetColor(Color::CornflowerBlue().WithAlpha(0.75));
2824 auto result = contents.ApplyColorFilter([](const Color& color) {
2825 return color.Blend(Color::LimeGreen().WithAlpha(0.75), BlendMode::kScreen);
2826 });
2827 ASSERT_TRUE(result);
2828 ASSERT_COLOR_NEAR(contents.GetColor(),
2829 Color(0.424452, 0.828743, 0.79105, 0.9375));
2830}
2831
2832#define APPLY_COLOR_FILTER_GRADIENT_TEST(name) \
2833 TEST_P(EntityTest, name##GradientApplyColorFilter) { \
2834 auto geom = Geometry::MakeCover(); \
2835 auto contents = name##GradientContents(geom.get()); \
2836 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.75)}); \
2837 auto result = contents.ApplyColorFilter([](const Color& color) { \
2838 return color.Blend(Color::LimeGreen().WithAlpha(0.75), \
2839 BlendMode::kScreen); \
2840 }); \
2841 ASSERT_TRUE(result); \
2842 \
2843 std::vector<Color> expected = {Color(0.433247, 0.879523, 0.825324, 0.75)}; \
2844 ASSERT_COLORS_NEAR(contents.GetColors(), expected); \
2845 }
2846
2851
2852TEST_P(EntityTest, GiantStrokePathAllocation) {
2853 flutter::DlPathBuilder builder;
2854 for (int i = 0; i < 10000; i++) {
2855 builder.LineTo(Point(i, i));
2856 }
2857 flutter::DlPath path = builder.TakePath();
2858 auto geom = Geometry::MakeStrokePath(path, {.width = 10.0f});
2859
2860 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2861 Entity entity;
2862
2863 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2864
2866 content_context.GetContext()->GetResourceAllocator());
2867
2868 auto render_target = allocator.CreateOffscreen(
2869 *content_context.GetContext(), /*size=*/{10, 10}, /*mip_count=*/1);
2870 auto pass = cmd_buffer->CreateRenderPass(render_target);
2871
2872 GeometryResult result =
2873 geom->GetPositionBuffer(content_context, entity, *pass);
2874
2875 // Validate the buffer data overflowed the small buffer
2876 EXPECT_GT(result.vertex_buffer.vertex_count, kPointArenaSize);
2877
2878 // Validate that there are no uninitialized points near the gap.
2879 Point* written_data = reinterpret_cast<Point*>(
2882
2883 std::vector<Point> expected = {
2884 Point(2043.46, 2050.54), //
2885 Point(2050.54, 2043.46), //
2886 Point(2044.46, 2051.54), //
2887 Point(2051.54, 2044.46), //
2888 Point(2045.46, 2052.54) //
2889 };
2890
2891 Point point = written_data[kPointArenaSize - 2];
2892 EXPECT_NEAR(point.x, expected[0].x, 0.1);
2893 EXPECT_NEAR(point.y, expected[0].y, 0.1);
2894
2895 point = written_data[kPointArenaSize - 1];
2896 EXPECT_NEAR(point.x, expected[1].x, 0.1);
2897 EXPECT_NEAR(point.y, expected[1].y, 0.1);
2898
2899 point = written_data[kPointArenaSize];
2900 EXPECT_NEAR(point.x, expected[2].x, 0.1);
2901 EXPECT_NEAR(point.y, expected[2].y, 0.1);
2902
2903 point = written_data[kPointArenaSize + 1];
2904 EXPECT_NEAR(point.x, expected[3].x, 0.1);
2905 EXPECT_NEAR(point.y, expected[3].y, 0.1);
2906
2907 point = written_data[kPointArenaSize + 2];
2908 EXPECT_NEAR(point.x, expected[4].x, 0.1);
2909 EXPECT_NEAR(point.y, expected[4].y, 0.1);
2910}
2911
2913 public:
2915 : DeviceBuffer(desc), storage_(desc.size) {}
2916
2917 bool SetLabel(std::string_view label) override { return true; }
2918 bool SetLabel(std::string_view label, Range range) override { return true; }
2919 bool OnCopyHostBuffer(const uint8_t* source,
2920 Range source_range,
2921 size_t offset) {
2922 return true;
2923 }
2924
2925 uint8_t* OnGetContents() const override {
2926 return const_cast<uint8_t*>(storage_.data());
2927 }
2928
2929 void Flush(std::optional<Range> range) const override {
2930 flush_called_ = true;
2931 }
2932
2933 bool flush_called() const { return flush_called_; }
2934
2935 private:
2936 std::vector<uint8_t> storage_;
2937 mutable bool flush_called_ = false;
2938};
2939
2941 public:
2943 return ISize(1024, 1024);
2944 };
2945
2946 std::shared_ptr<DeviceBuffer> OnCreateBuffer(
2947 const DeviceBufferDescriptor& desc) override {
2948 return std::make_shared<FlushTestDeviceBuffer>(desc);
2949 };
2950
2951 std::shared_ptr<Texture> OnCreateTexture(const TextureDescriptor& desc,
2952 bool threadsafe) override {
2953 return nullptr;
2954 }
2955};
2956
2958 public:
2960 const std::shared_ptr<Context>& context,
2961 const std::shared_ptr<TypographerContext>& typographer_context,
2962 const std::shared_ptr<Allocator>& allocator)
2963 : ContentContext(context, typographer_context) {
2965 allocator, context->GetIdleWaiter(),
2966 context->GetCapabilities()->GetMinimumUniformAlignment()));
2968 allocator, context->GetIdleWaiter(),
2969 context->GetCapabilities()->GetMinimumUniformAlignment()));
2970 }
2971};
2972
2973TEST_P(EntityTest, RoundSuperellipseGetPositionBufferFlushes) {
2975 testing::MockRenderPass mock_pass(GetContext(), target);
2976
2977 auto content_context = std::make_shared<FlushTestContentContext>(
2978 GetContext(), GetTypographerContext(),
2979 std::make_shared<FlushTestAllocator>());
2980 auto geometry =
2982 auto result = geometry->GetPositionBuffer(*content_context, {}, mock_pass);
2983
2984 auto device_buffer = reinterpret_cast<const FlushTestDeviceBuffer*>(
2985 result.vertex_buffer.vertex_buffer.GetBuffer());
2986 EXPECT_TRUE(device_buffer->flush_called());
2987}
2988
2989} // namespace testing
2990} // namespace impeller
2991
2992// NOLINTEND(bugprone-unchecked-optional-access)
DlPathBuilder & LineTo(DlPoint p2)
Draw a line from the current point to the indicated point p2.
DlPathBuilder & MoveTo(DlPoint p2)
Start a new contour that will originate at the indicated point p2.
const DlPath TakePath()
Returns the path constructed by this path builder and resets its internal state to the default state ...
DlPathBuilder & Close()
The path is closed back to the location of the most recent MoveTo call. Contours that are filled are ...
static DlPath MakeLine(const DlPoint a, const DlPoint b)
Definition dl_path.cc:89
static DlPath MakeRect(const DlRect &rect)
Definition dl_path.cc:39
static DlPath MakeRoundSuperellipse(const DlRoundSuperellipse &rse)
Definition dl_path.cc:85
double x() const
Definition geometry.h:22
double y() const
Definition geometry.h:23
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)
const std::shared_ptr< RenderTargetAllocator > & GetRenderTargetCache() const
PipelineRef GetMorphologyFilterPipeline(ContentContextOptions opts) const
PipelineRef GetGlyphAtlasPipeline(ContentContextOptions opts) const
void SetTransientsIndexesBuffer(std::shared_ptr< HostBuffer > host_buffer)
std::shared_ptr< Context > GetContext() const
virtual bool IsOpaque(const Matrix &transform) const
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
Definition contents.cc:52
To do anything rendering related with Impeller, you need a context.
Definition context.h:70
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)
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, std::shared_ptr< const GpuSubmissionTracker > submission_tracker=nullptr)
void SetTileMode(Entity::TileMode tile_mode)
void SetColors(std::vector< Color > colors)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
A geometry class specialized for Canvas::DrawPoints.
std::optional< Rect > GetCoverage(const Matrix &transform) const override
The coverage rectangle of this geometry, transformed by the transform argument.
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetTileMode(Entity::TileMode tile_mode)
void SetColors(std::vector< Color > colors)
Render passes encode render commands directed as one specific render target into an underlying comman...
Definition render_pass.h:30
virtual const std::vector< Command > & GetCommands() const
Accessor for the current Commands.
a wrapper around the impeller [Allocator] instance that can be used to provide caching of allocated r...
static BufferView EmplaceUniform(const uint8_t *source_data, HostBuffer &host_buffer, const RuntimeUniformDescription &uniform)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
A Geometry that produces fillable vertices representing the stroked outline of a |DlPath| object usin...
void SetTileMode(Entity::TileMode tile_mode)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetColors(std::vector< Color > colors)
static Rational RoundScaledFontSize(Scalar scale)
Definition text_frame.cc:55
static std::shared_ptr< TextureContents > MakeRect(Rect destination)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
void SetTexture(std::shared_ptr< Texture > texture)
static std::unique_ptr< UberSDFContents > Make(const UberSDFParameters &params, std::unique_ptr< Geometry > geometry)
VertexBuffer CreateVertexBuffer(HostBuffer &data_host_buffer, HostBuffer &indexes_host_buffer) const
VertexBufferBuilder & AddVertices(std::initializer_list< VertexType_ > vertices)
std::shared_ptr< DeviceBuffer > OnCreateBuffer(const DeviceBufferDescriptor &desc) override
std::shared_ptr< Texture > OnCreateTexture(const TextureDescriptor &desc, bool threadsafe) 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)
static int input(yyscan_t yyscanner)
const EmbeddedViewParams * params
FlutterVulkanImage * image
#define APPLY_COLOR_FILTER_GRADIENT_TEST(name)
auto & d
Definition main.cc:28
uint32_t * target
FlutterDesktopBinaryReply callback
#define FML_DLOG(severity)
Definition logging.h:121
#define FML_CHECK(condition)
Definition logging.h:104
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)
std::shared_ptr< ImpellerAllocator > allocator
FlTexture * texture
double y
TEST(FrameTimingsRecorderTest, RecordVsync)
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition switch_defs.h:52
DEF_SWITCHES_START aot vmservice shared library name
Definition switch_defs.h:27
static constexpr DlScalar kPi
static constexpr DlScalar kEhCloseEnough
TEST_P(AiksTest, DrawAtlasNoColor)
static std::vector< std::shared_ptr< Texture > > CreateTestYUVTextures(Context *context, YUVColorSpace yuv_color_space)
static Vector3 RGBToYUV(Vector3 rgb, 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
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
GlyphAtlasPipeline::VertexShader VS
Cap
An enum that describes ways to decorate the end of a path contour.
constexpr float kPiOver2
Definition constants.h:32
BlendMode
Definition color.h:58
FramebufferBlendPipelineHandle FramebufferBlendColorBurnPipeline
Definition pipelines.h:121
void MoveTo(PathBuilder *builder, Scalar x, Scalar y)
GlyphAtlasPipeline::FragmentShader FS
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)
ContentContextOptions OptionsFromPass(const RenderPass &pass)
Definition contents.cc:19
ISize64 ISize
Definition size.h:162
void Close(PathBuilder *builder)
#define INSTANTIATE_PLAYGROUND_SUITE(playground)
std::shared_ptr< ContextGLES > context
std::shared_ptr< RenderPass > render_pass
int32_t height
int32_t width
Range GetRange() const
Definition buffer_view.h:27
const DeviceBuffer * GetBuffer() const
static constexpr Color LimeGreen()
Definition color.h:607
static constexpr Color MintCream()
Definition color.h:663
Scalar alpha
Definition color.h:143
static constexpr Color DeepPink()
Definition color.h:439
static constexpr Color Black()
Definition color.h:271
static constexpr Color CornflowerBlue()
Definition color.h:347
static constexpr Color White()
Definition color.h:269
constexpr Color WithAlpha(Scalar new_alpha) const
Definition color.h:283
static constexpr Color WhiteTransparent()
Definition color.h:273
static constexpr Color Coral()
Definition color.h:343
static constexpr Color Red()
Definition color.h:277
constexpr Color Premultiply() const
Definition color.h:212
Color Blend(Color source, BlendMode blend_mode) const
Blends an unpremultiplied destination color into a given unpremultiplied source color to form a new u...
Definition color.cc:155
static constexpr Color Blue()
Definition color.h:281
static constexpr Color Green()
Definition color.h:279
Scalar array[20]
Definition color.h:118
constexpr uint64_t ToKey() const
@ kNormal
The geometry has no overlapping triangles.
VertexBuffer vertex_buffer
Definition geometry.h:38
A 4x4 matrix using column-major storage.
Definition matrix.h:37
static constexpr Matrix MakeTranslation(const Vector3 &t)
Definition matrix.h:95
constexpr bool IsIdentity() const
Definition matrix.h:475
Matrix Invert() const
Definition matrix.cc:99
static Matrix MakeRotationY(Radians r)
Definition matrix.h:208
static constexpr Matrix MakeSkew(Scalar sx, Scalar sy)
Definition matrix.h:127
Scalar e[4][4]
Definition matrix.h:40
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.
static constexpr TRect MakeEllipseBounds(const TPoint< Type > &center, const TSize< Type > &radii)
Definition rect.h:164
static constexpr TRect MakeXYWH(Type x, Type y, Type width, Type height)
Definition rect.h:136
static constexpr TRect MakeSize(const TSize< U > &size)
Definition rect.h:150
constexpr TRect< T > Expand(T left, T top, T right, T bottom) const
Returns a rectangle with expanded edges. Negative expansion results in shrinking.
Definition rect.h:652
static constexpr TRect MakeLTRB(Type left, Type top, Type right, Type bottom)
Definition rect.h:129
A lightweight object that describes the attributes of a texture that can then used an allocator to cr...
static UberSDFParameters MakeCircle(Color color, const Point &center, Scalar radius, std::optional< StrokeParameters > stroke)
Creates UberSDFParameters for a circle.
static UberSDFParameters MakeRect(Color color, const Rect &rect, std::optional< StrokeParameters > stroke)
Creates UberSDFParameters for a rectangle.
const size_t start
std::vector< Point > points
Scalar font_size