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