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(
1692 Context* context,
1693 YUVColorSpace yuv_color_space) {
1694 Vector3 red = {244.0 / 255.0, 67.0 / 255.0, 54.0 / 255.0};
1695 Vector3 green = {76.0 / 255.0, 175.0 / 255.0, 80.0 / 255.0};
1696 Vector3 blue = {33.0 / 255.0, 150.0 / 255.0, 243.0 / 255.0};
1697 Vector3 white = {1.0, 1.0, 1.0};
1698 Vector3 red_yuv = RGBToYUV(red, yuv_color_space);
1699 Vector3 green_yuv = RGBToYUV(green, yuv_color_space);
1700 Vector3 blue_yuv = RGBToYUV(blue, yuv_color_space);
1701 Vector3 white_yuv = RGBToYUV(white, yuv_color_space);
1702 std::vector<Vector3> yuvs{red_yuv, green_yuv, blue_yuv, white_yuv};
1703 std::vector<uint8_t> y_data;
1704 std::vector<uint8_t> uv_data;
1705 for (int i = 0; i < 4; i++) {
1706 auto yuv = yuvs[i];
1707 uint8_t y = std::round(yuv.x * 255.0);
1708 uint8_t u = std::round(yuv.y * 255.0);
1709 uint8_t v = std::round(yuv.z * 255.0);
1710 for (int j = 0; j < 16; j++) {
1711 y_data.push_back(y);
1712 }
1713 for (int j = 0; j < 8; j++) {
1714 uv_data.push_back(j % 2 == 0 ? u : v);
1715 }
1716 }
1717 auto cmd_buffer = context->CreateCommandBuffer();
1718 auto blit_pass = cmd_buffer->CreateBlitPass();
1719
1720 impeller::TextureDescriptor y_texture_descriptor;
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 // TODO(114588) : Support YUV to RGB filter on OpenGLES backend.
1755 GTEST_SKIP()
1756 << "YUV to RGB filter is not supported on OpenGLES backend yet.";
1757 }
1758
1759 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1760 YUVColorSpace yuv_color_space_array[2]{YUVColorSpace::kBT601FullRange,
1762 for (int i = 0; i < 2; i++) {
1763 auto yuv_color_space = yuv_color_space_array[i];
1764 auto textures =
1765 CreateTestYUVTextures(GetContext().get(), yuv_color_space);
1766 auto filter_contents = FilterContents::MakeYUVToRGBFilter(
1767 textures[0], textures[1], yuv_color_space);
1768 Entity filter_entity;
1769 filter_entity.SetContents(filter_contents);
1770 auto snapshot =
1771 filter_contents->RenderToSnapshot(context, filter_entity, {});
1772
1773 Entity entity;
1774 auto contents = TextureContents::MakeRect(Rect::MakeLTRB(0, 0, 256, 256));
1775 contents->SetTexture(snapshot->texture);
1776 contents->SetSourceRect(Rect::MakeSize(snapshot->texture->GetSize()));
1777 entity.SetContents(contents);
1778 entity.SetTransform(
1779 Matrix::MakeTranslation({static_cast<Scalar>(100 + 400 * i), 300}));
1780 entity.Render(context, pass);
1781 }
1782 return true;
1783 };
1784 ASSERT_TRUE(OpenPlaygroundHere(callback));
1785}
1786
1787TEST_P(EntityTest, RuntimeEffect) {
1788 auto runtime_stages_result =
1789 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1790 ABSL_ASSERT_OK(runtime_stages_result);
1791 std::shared_ptr<RuntimeStage> runtime_stage =
1792 runtime_stages_result.value()[GetRuntimeStageBackend()];
1793 ASSERT_TRUE(runtime_stage);
1794 ASSERT_TRUE(runtime_stage->IsDirty());
1795
1796 bool expect_dirty = true;
1797
1798 PipelineRef first_pipeline;
1799 std::unique_ptr<Geometry> geom = Geometry::MakeCover();
1800
1801 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1802 EXPECT_EQ(runtime_stage->IsDirty(), expect_dirty);
1803
1804 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1805 contents->SetRuntimeStage(runtime_stage);
1806
1807 struct FragUniforms {
1808 Vector2 iResolution;
1809 Scalar iTime;
1810 } frag_uniforms = {
1811 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1812 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1813 };
1814 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1815 uniform_data->resize(sizeof(FragUniforms));
1816 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1817 contents->SetUniformData(uniform_data);
1818
1819 Entity entity;
1820 entity.SetContents(contents);
1821 bool result = contents->Render(context, entity, pass);
1822
1823 if (expect_dirty) {
1824 first_pipeline = pass.GetCommands().back().pipeline;
1825 } else {
1826 EXPECT_EQ(pass.GetCommands().back().pipeline, first_pipeline);
1827 }
1828 expect_dirty = false;
1829 return result;
1830 };
1831
1832 // Simulate some renders and hot reloading of the shader.
1833 auto content_context = GetContentContext();
1834 {
1836 content_context->GetRenderTargetCache()->CreateOffscreen(
1837 *content_context->GetContext(), {1, 1}, 1u);
1838
1839 testing::MockRenderPass mock_pass(GetContext(), target);
1840 callback(*content_context, mock_pass);
1841 callback(*content_context, mock_pass);
1842
1843 // Dirty the runtime stage.
1844 auto runtime_stages_result =
1845 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1846 ABSL_ASSERT_OK(runtime_stages_result);
1847 runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1848
1849 ASSERT_TRUE(runtime_stage->IsDirty());
1850 expect_dirty = true;
1851
1852 callback(*content_context, mock_pass);
1853 }
1854}
1855
1856TEST_P(EntityTest, RuntimeEffectCanSuccessfullyRender) {
1857 auto runtime_stages_result =
1858 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1859 ABSL_ASSERT_OK(runtime_stages_result);
1860 auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1861 ASSERT_TRUE(runtime_stage);
1862 ASSERT_TRUE(runtime_stage->IsDirty());
1863
1864 auto geom = Geometry::MakeCover();
1865 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1866 contents->SetRuntimeStage(runtime_stage);
1867
1868 struct FragUniforms {
1869 Vector2 iResolution;
1870 Scalar iTime;
1871 } frag_uniforms = {
1872 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1873 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1874 };
1875 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1876 uniform_data->resize(sizeof(FragUniforms));
1877 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1878 contents->SetUniformData(uniform_data);
1879
1880 Entity entity;
1881 entity.SetContents(contents);
1882
1883 // Create a render target with a depth-stencil, similar to how EntityPass
1884 // does.
1886 GetContentContext()->GetRenderTargetCache()->CreateOffscreenMSAA(
1887 *GetContext(), {GetWindowSize().width, GetWindowSize().height}, 1,
1888 "RuntimeEffect Texture");
1889 testing::MockRenderPass pass(GetContext(), target);
1890
1891 ASSERT_TRUE(contents->Render(*GetContentContext(), entity, pass));
1892 ASSERT_EQ(pass.GetCommands().size(), 1u);
1893 const auto& command = pass.GetCommands()[0];
1894 ASSERT_TRUE(command.pipeline->GetDescriptor()
1895 .GetDepthStencilAttachmentDescriptor()
1896 .has_value());
1897 ASSERT_TRUE(command.pipeline->GetDescriptor()
1898 .GetFrontStencilAttachmentDescriptor()
1899 .has_value());
1900}
1901
1902TEST_P(EntityTest, RuntimeEffectCanPrecache) {
1903 auto runtime_stages_result =
1904 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1905 ABSL_ASSERT_OK(runtime_stages_result);
1906 std::shared_ptr<RuntimeStage> runtime_stage =
1907 runtime_stages_result.value()[GetRuntimeStageBackend()];
1908 ASSERT_TRUE(runtime_stage);
1909 ASSERT_TRUE(runtime_stage->IsDirty());
1910
1911 auto geom = Geometry::MakeCover();
1912 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1913 contents->SetRuntimeStage(runtime_stage);
1914
1915 EXPECT_TRUE(contents->BootstrapShader(*GetContentContext()));
1916}
1917
1918TEST_P(EntityTest, RuntimeEffectSetsRightSizeWhenUniformIsStruct) {
1919 if (GetBackend() != PlaygroundBackend::kVulkan) {
1920 GTEST_SKIP() << "Test only applies to Vulkan";
1921 }
1922
1923 auto runtime_stages_result =
1924 OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
1925 ABSL_ASSERT_OK(runtime_stages_result);
1926 auto runtime_stage = runtime_stages_result.value()[GetRuntimeStageBackend()];
1927 ASSERT_TRUE(runtime_stage);
1928 ASSERT_TRUE(runtime_stage->IsDirty());
1929
1930 auto geom = Geometry::MakeCover();
1931 auto contents = std::make_shared<RuntimeEffectContents>(geom.get());
1932 contents->SetRuntimeStage(runtime_stage);
1933
1934 struct FragUniforms {
1935 Vector2 iResolution;
1936 Scalar iTime;
1937 } frag_uniforms = {
1938 .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height),
1939 .iTime = static_cast<Scalar>(GetSecondsElapsed()),
1940 };
1941 auto uniform_data = std::make_shared<std::vector<uint8_t>>();
1942 uniform_data->resize(sizeof(FragUniforms));
1943 memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
1944
1945 auto buffer_view = RuntimeEffectContents::EmplaceUniform(
1946 uniform_data->data(), GetContentContext()->GetTransientsDataBuffer(),
1947 runtime_stage->GetUniforms()[0]);
1948
1949 // 16 bytes:
1950 // 8 bytes for iResolution
1951 // 4 bytes for iTime
1952 // 4 bytes padding
1953 EXPECT_EQ(buffer_view.GetRange().length, 16u);
1954}
1955
1956TEST_P(EntityTest, ColorFilterWithForegroundColorAdvancedBlend) {
1957 auto image = CreateTextureForFixture("boston.jpg");
1958 auto filter = ColorFilterContents::MakeBlend(
1960
1961 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1962 Entity entity;
1963 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1964 Matrix::MakeTranslation({500, 300}) *
1965 Matrix::MakeScale(Vector2{0.5, 0.5}));
1966 entity.SetContents(filter);
1967 return entity.Render(context, pass);
1968 };
1969 ASSERT_TRUE(OpenPlaygroundHere(callback));
1970}
1971
1972TEST_P(EntityTest, ColorFilterWithForegroundColorClearBlend) {
1973 auto image = CreateTextureForFixture("boston.jpg");
1974 auto filter = ColorFilterContents::MakeBlend(
1976
1977 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1978 Entity entity;
1979 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1980 Matrix::MakeTranslation({500, 300}) *
1981 Matrix::MakeScale(Vector2{0.5, 0.5}));
1982 entity.SetContents(filter);
1983 return entity.Render(context, pass);
1984 };
1985 ASSERT_TRUE(OpenPlaygroundHere(callback));
1986}
1987
1988TEST_P(EntityTest, ColorFilterWithForegroundColorSrcBlend) {
1989 auto image = CreateTextureForFixture("boston.jpg");
1990 auto filter = ColorFilterContents::MakeBlend(
1992
1993 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
1994 Entity entity;
1995 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
1996 Matrix::MakeTranslation({500, 300}) *
1997 Matrix::MakeScale(Vector2{0.5, 0.5}));
1998 entity.SetContents(filter);
1999 return entity.Render(context, pass);
2000 };
2001 ASSERT_TRUE(OpenPlaygroundHere(callback));
2002}
2003
2004TEST_P(EntityTest, ColorFilterWithForegroundColorDstBlend) {
2005 auto image = CreateTextureForFixture("boston.jpg");
2006 auto filter = ColorFilterContents::MakeBlend(
2008
2009 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2010 Entity entity;
2011 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2012 Matrix::MakeTranslation({500, 300}) *
2013 Matrix::MakeScale(Vector2{0.5, 0.5}));
2014 entity.SetContents(filter);
2015 return entity.Render(context, pass);
2016 };
2017 ASSERT_TRUE(OpenPlaygroundHere(callback));
2018}
2019
2020TEST_P(EntityTest, ColorFilterWithForegroundColorSrcInBlend) {
2021 auto image = CreateTextureForFixture("boston.jpg");
2022 auto filter = ColorFilterContents::MakeBlend(
2024
2025 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2026 Entity entity;
2027 entity.SetTransform(Matrix::MakeScale(GetContentScale()) *
2028 Matrix::MakeTranslation({500, 300}) *
2029 Matrix::MakeScale(Vector2{0.5, 0.5}));
2030 entity.SetContents(filter);
2031 return entity.Render(context, pass);
2032 };
2033 ASSERT_TRUE(OpenPlaygroundHere(callback));
2034}
2035
2036TEST_P(EntityTest, CoverageForStrokePathWithNegativeValuesInTransform) {
2037 auto arrow_head = flutter::DlPathBuilder{}
2038 .MoveTo({50, 120})
2039 .LineTo({120, 190})
2040 .LineTo({190, 120})
2041 .TakePath();
2042 auto geometry = Geometry::MakeStrokePath(arrow_head, //
2043 {
2044 .width = 15.0f,
2045 .cap = Cap::kRound,
2046 .join = Join::kRound,
2047 .miter_limit = 4.0f,
2048 });
2049
2050 auto transform = Matrix::MakeTranslation({300, 300}) *
2052 // Note that e[0][0] used to be tested here, but it was -epsilon solely
2053 // due to floating point inaccuracy in the transcendental trig functions.
2054 // e[1][0] is the intended negative value that we care about (-1.0) as it
2055 // comes from the rotation of pi/2.
2056 EXPECT_LT(transform.e[1][0], 0.0f);
2057 auto coverage = geometry->GetCoverage(transform);
2058 ASSERT_RECT_NEAR(coverage.value(), Rect::MakeXYWH(102.5, 342.5, 85, 155));
2059}
2060
2061TEST_P(EntityTest, SolidColorContentsIsOpaque) {
2062 Matrix matrix;
2063 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2064 SolidColorContents contents(geom.get());
2065
2066 contents.SetColor(Color::CornflowerBlue());
2067 EXPECT_TRUE(contents.IsOpaque(matrix));
2068 contents.SetColor(Color::CornflowerBlue().WithAlpha(0.5));
2069 EXPECT_FALSE(contents.IsOpaque(matrix));
2070
2071 // Create stroked path that required alpha coverage.
2072 auto geom2 = Geometry::MakeStrokePath(
2073 flutter::DlPath::MakeLine({0, 0}, {100, 100}), {.width = 0.05});
2074 SolidColorContents contents2(geom2.get());
2075 contents2.SetColor(Color::CornflowerBlue());
2076
2077 EXPECT_FALSE(contents2.IsOpaque(matrix));
2078}
2079
2080TEST_P(EntityTest, ConicalGradientContentsIsOpaque) {
2081 Matrix matrix;
2082 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2083 ConicalGradientContents contents(geom.get());
2084
2085 contents.SetColors({Color::CornflowerBlue()});
2086 EXPECT_FALSE(contents.IsOpaque(matrix));
2087 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2088 EXPECT_FALSE(contents.IsOpaque(matrix));
2089
2090 // Create stroked path that required alpha coverage.
2091 auto geom2 = Geometry::MakeStrokePath(
2092 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2093 {.width = 0.05f});
2094 ConicalGradientContents contents2(geom2.get());
2095 contents2.SetColors({Color::CornflowerBlue()});
2096
2097 EXPECT_FALSE(contents2.IsOpaque(matrix));
2098}
2099
2100TEST_P(EntityTest, LinearGradientContentsIsOpaque) {
2101 Matrix matrix;
2102 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2103 LinearGradientContents contents(geom.get());
2104
2105 contents.SetColors({Color::CornflowerBlue()});
2106 EXPECT_TRUE(contents.IsOpaque(matrix));
2107 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2108 EXPECT_FALSE(contents.IsOpaque(matrix));
2109 contents.SetColors({Color::CornflowerBlue()});
2111 EXPECT_FALSE(contents.IsOpaque(matrix));
2112
2113 // Create stroked path that required alpha coverage.
2114 auto geom2 = Geometry::MakeStrokePath(
2115 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2116 {.width = 0.05f});
2117 LinearGradientContents contents2(geom2.get());
2118 contents2.SetColors({Color::CornflowerBlue()});
2119
2120 EXPECT_FALSE(contents2.IsOpaque(matrix));
2121}
2122
2123TEST_P(EntityTest, RadialGradientContentsIsOpaque) {
2124 Matrix matrix;
2125 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2126 RadialGradientContents contents(geom.get());
2127
2128 contents.SetColors({Color::CornflowerBlue()});
2129 EXPECT_TRUE(contents.IsOpaque(matrix));
2130 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2131 EXPECT_FALSE(contents.IsOpaque(matrix));
2132 contents.SetColors({Color::CornflowerBlue()});
2134 EXPECT_FALSE(contents.IsOpaque(matrix));
2135
2136 // Create stroked path that required alpha coverage.
2137 auto geom2 = Geometry::MakeStrokePath(
2138 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2139 {.width = 0.05});
2140 RadialGradientContents contents2(geom2.get());
2141 contents2.SetColors({Color::CornflowerBlue()});
2142
2143 EXPECT_FALSE(contents2.IsOpaque(matrix));
2144}
2145
2146TEST_P(EntityTest, SweepGradientContentsIsOpaque) {
2147 Matrix matrix;
2148 auto geom = Geometry::MakeRect(Rect::MakeLTRB(0, 0, 10, 10));
2149 SweepGradientContents contents(geom.get());
2150
2151 contents.SetColors({Color::CornflowerBlue()});
2152 EXPECT_TRUE(contents.IsOpaque(matrix));
2153 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
2154 EXPECT_FALSE(contents.IsOpaque(matrix));
2155 contents.SetColors({Color::CornflowerBlue()});
2157 EXPECT_FALSE(contents.IsOpaque(matrix));
2158
2159 // Create stroked path that required alpha coverage.
2160 auto geom2 = Geometry::MakeStrokePath(
2161 flutter::DlPathBuilder{}.MoveTo({0, 0}).LineTo({100, 100}).TakePath(),
2162 {.width = 0.05f});
2163 SweepGradientContents contents2(geom2.get());
2164 contents2.SetColors({Color::CornflowerBlue()});
2165
2166 EXPECT_FALSE(contents2.IsOpaque(matrix));
2167}
2168
2169TEST_P(EntityTest, TiledTextureContentsIsOpaque) {
2170 Matrix matrix;
2171 auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg");
2172 auto geom = Geometry::MakeCover();
2173 TiledTextureContents contents(geom.get());
2174 contents.SetTexture(bay_bridge);
2175 // This is a placeholder test. Images currently never decompress as opaque
2176 // (whether in Flutter or the playground), and so this should currently always
2177 // return false in practice.
2178 EXPECT_FALSE(contents.IsOpaque(matrix));
2179}
2180
2181TEST_P(EntityTest, PointFieldGeometryCoverage) {
2182 std::vector<Point> points = {{10, 20}, {100, 200}};
2183 PointFieldGeometry geometry(points.data(), 2, 5.0, false);
2184 ASSERT_EQ(geometry.GetCoverage(Matrix()), Rect::MakeLTRB(5, 15, 105, 205));
2185 ASSERT_EQ(geometry.GetCoverage(Matrix::MakeTranslation({30, 0, 0})),
2186 Rect::MakeLTRB(35, 15, 135, 205));
2187}
2188
2189TEST_P(EntityTest, ColorFilterContentsWithLargeGeometry) {
2190 Entity entity;
2191 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2192 auto src_geom = Geometry::MakeRect(Rect::MakeLTRB(-300, -500, 30000, 50000));
2193 auto src_contents = std::make_shared<SolidColorContents>(src_geom.get());
2194 src_contents->SetColor(Color::Red());
2195
2196 auto dst_geom = Geometry::MakeRect(Rect::MakeLTRB(300, 500, 20000, 30000));
2197 auto dst_contents = std::make_shared<SolidColorContents>(dst_geom.get());
2198 dst_contents->SetColor(Color::Blue());
2199
2200 auto contents = ColorFilterContents::MakeBlend(
2201 BlendMode::kSrcOver, {FilterInput::Make(dst_contents, false),
2202 FilterInput::Make(src_contents, false)});
2203 entity.SetContents(std::move(contents));
2204 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2205}
2206
2207TEST_P(EntityTest, TextContentsCeilsGlyphScaleToDecimal) {
2208 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.4321111f), Rational(43, 100));
2209 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.5321111f), Rational(53, 100));
2210 ASSERT_EQ(TextFrame::RoundScaledFontSize(2.1f), Rational(21, 10));
2211 ASSERT_EQ(TextFrame::RoundScaledFontSize(0.0f), Rational(0, 1));
2212 ASSERT_EQ(TextFrame::RoundScaledFontSize(100000000.0f), Rational(48, 1));
2213}
2214
2215TEST_P(EntityTest, SpecializationConstantsAreAppliedToVariants) {
2216 auto content_context = GetContentContext();
2217
2218 auto default_gyph = content_context->GetGlyphAtlasPipeline({
2219 .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2220 .has_depth_stencil_attachments = false,
2221 });
2222 auto alt_gyph = content_context->GetGlyphAtlasPipeline(
2223 {.color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2224 .has_depth_stencil_attachments = true});
2225
2226 EXPECT_NE(default_gyph, alt_gyph);
2227 EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2228 alt_gyph->GetDescriptor().GetSpecializationConstants());
2229
2230 auto use_a8 = GetContext()->GetCapabilities()->GetDefaultGlyphAtlasFormat() ==
2232
2233 std::vector<Scalar> expected_constants = {static_cast<Scalar>(use_a8)};
2234 EXPECT_EQ(default_gyph->GetDescriptor().GetSpecializationConstants(),
2235 expected_constants);
2236}
2237
2238TEST_P(EntityTest, DecalSpecializationAppliedToMorphologyFilter) {
2239 auto content_context = GetContentContext();
2240 auto default_color_burn = content_context->GetMorphologyFilterPipeline({
2241 .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt,
2242 });
2243
2244 auto decal_supported = static_cast<Scalar>(
2245 GetContext()->GetCapabilities()->SupportsDecalSamplerAddressMode());
2246 std::vector<Scalar> expected_constants = {decal_supported};
2247 ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(),
2248 expected_constants);
2249}
2250
2251// This doesn't really tell you if the hashes will have frequent
2252// collisions, but since this type is only used to hash a bounded
2253// set of options, we can just compare benchmarks.
2254TEST_P(EntityTest, ContentContextOptionsHasReasonableHashFunctions) {
2256 auto hash_a = opts.ToKey();
2257
2259 auto hash_b = opts.ToKey();
2260
2261 opts.has_depth_stencil_attachments = false;
2262 auto hash_c = opts.ToKey();
2263
2265 auto hash_d = opts.ToKey();
2266
2267 EXPECT_NE(hash_a, hash_b);
2268 EXPECT_NE(hash_b, hash_c);
2269 EXPECT_NE(hash_c, hash_d);
2270}
2271
2272#ifdef FML_OS_LINUX
2273TEST_P(EntityTest, FramebufferFetchVulkanBindingOffsetIsTheSame) {
2274 // Using framebuffer fetch on Vulkan requires that we maintain a subpass input
2275 // binding that we don't have a good route for configuring with the
2276 // current metadata approach. This test verifies that the binding value
2277 // doesn't change
2278 // from the expected constant.
2279 // See also:
2280 // * impeller/renderer/backend/vulkan/binding_helpers_vk.cc
2281 // * impeller/entity/shaders/blending/framebuffer_blend.frag
2282 // This test only works on Linux because macOS hosts incorrectly
2283 // populate the
2284 // Vulkan descriptor sets based on the MSL compiler settings.
2285
2286 bool expected_layout = false;
2288 FragmentShader::kDescriptorSetLayouts) {
2289 if (layout.binding == 64 &&
2290 layout.descriptor_type == DescriptorType::kInputAttachment) {
2291 expected_layout = true;
2292 }
2293 }
2294 EXPECT_TRUE(expected_layout);
2295}
2296#endif
2297
2298TEST_P(EntityTest, FillPathGeometryGetPositionBufferReturnsExpectedMode) {
2300 testing::MockRenderPass mock_pass(GetContext(), target);
2301
2302 auto get_result = [this, &mock_pass](const flutter::DlPath& path) {
2303 auto geometry = Geometry::MakeFillPath(
2304 path, /* inner rect */ Rect::MakeLTRB(0, 0, 100, 100));
2305 return geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2306 };
2307
2308 // Convex path
2309 {
2310 GeometryResult result =
2311 get_result(flutter::DlPath::MakeRect(Rect::MakeLTRB(0, 0, 100, 100)));
2312 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal);
2313 }
2314
2315 // Concave path
2316 {
2318 .MoveTo({0, 0})
2319 .LineTo({100, 0})
2320 .LineTo({100, 100})
2321 .LineTo({51, 50})
2322 .Close()
2323 .TakePath();
2324 GeometryResult result = get_result(path);
2325 EXPECT_EQ(result.mode, GeometryResult::Mode::kNonZero);
2326 }
2327}
2328
2329TEST_P(EntityTest, StrokeArcGeometryGetPositionBufferReturnsExpectedMode) {
2331 testing::MockRenderPass mock_pass(GetContext(), target);
2332 Rect oval_bounds = Rect::MakeLTRB(100, 100, 200, 200);
2333
2334 // Butt caps never overlap
2335 {
2336 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kButt};
2337 for (auto start = 0; start < 360; start += 60) {
2338 for (auto sweep = 0; sweep < 360; sweep += 12) {
2339 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2340 Degrees(sweep), stroke);
2341
2342 GeometryResult result =
2343 geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2344
2345 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2346 << "start: " << start << " sweep: " << sweep;
2347 }
2348 }
2349 }
2350
2351 // Round caps with 10 stroke width overlap starting at 348.6 degrees
2352 {
2353 StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kRound};
2354 for (auto start = 0; start < 360; start += 60) {
2355 for (auto sweep = 0; sweep < 360; sweep += 12) {
2356 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2357 Degrees(sweep), stroke);
2358
2359 GeometryResult result =
2360 geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2361
2362 if (sweep < 348.6) {
2363 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2364 << "start: " << start << " sweep: " << sweep;
2365 } else {
2367 << "start: " << start << " sweep: " << sweep;
2368 }
2369 }
2370 }
2371 }
2372
2373 // Round caps with 50 stroke width overlap starting at 300.1 degrees
2374 {
2375 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kRound};
2376 for (auto start = 0; start < 360; start += 60) {
2377 for (auto sweep = 0; sweep < 360; sweep += 12) {
2378 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2379 Degrees(sweep), stroke);
2380
2381 GeometryResult result =
2382 geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2383
2384 if (sweep < 300.0) {
2385 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2386 << "start: " << start << " sweep: " << sweep;
2387 } else {
2389 << "start: " << start << " sweep: " << sweep;
2390 }
2391 }
2392 }
2393 }
2394
2395 // Square caps with 10 stroke width overlap starting at 347.4 degrees
2396 {
2397 StrokeParameters stroke = {.width = 10.0f, .cap = Cap::kSquare};
2398 for (auto start = 0; start < 360; start += 60) {
2399 for (auto sweep = 0; sweep < 360; sweep += 12) {
2400 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2401 Degrees(sweep), stroke);
2402
2403 GeometryResult result =
2404 geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2405
2406 if (sweep < 347.4) {
2407 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2408 << "start: " << start << " sweep: " << sweep;
2409 } else {
2411 << "start: " << start << " sweep: " << sweep;
2412 }
2413 }
2414 }
2415 }
2416
2417 // Square caps with 50 stroke width overlap starting at 270.1 degrees
2418 {
2419 StrokeParameters stroke = {.width = 50.0f, .cap = Cap::kSquare};
2420 for (auto start = 0; start < 360; start += 60) {
2421 for (auto sweep = 0; sweep < 360; sweep += 12) {
2422 auto geometry = Geometry::MakeStrokedArc(oval_bounds, Degrees(start),
2423 Degrees(sweep), stroke);
2424
2425 GeometryResult result =
2426 geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass);
2427
2428 if (sweep < 270.1) {
2429 EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal)
2430 << "start: " << start << " sweep: " << sweep;
2431 } else {
2433 << "start: " << start << " sweep: " << sweep;
2434 }
2435 }
2436 }
2437 }
2438}
2439
2440TEST_P(EntityTest, FailOnValidationError) {
2441 if (GetParam() != PlaygroundBackend::kVulkan) {
2442 GTEST_SKIP() << "Validation is only fatal on Vulkan backend.";
2443 }
2444 EXPECT_DEATH(
2445 // The easiest way to trigger a validation error is to try to compile
2446 // a shader with an unsupported pixel format.
2447 GetContentContext()->GetBlendColorBurnPipeline({
2448 .color_attachment_pixel_format = PixelFormat::kUnknown,
2449 .has_depth_stencil_attachments = false,
2450 }),
2451 "");
2452}
2453
2454TEST_P(EntityTest, CanComputeGeometryForEmptyPathsWithoutCrashing) {
2456
2457 EXPECT_TRUE(path.GetBounds().IsEmpty());
2458
2459 auto geom = Geometry::MakeFillPath(path);
2460
2461 Entity entity;
2463 GetContentContext()->GetRenderTargetCache()->CreateOffscreen(
2464 *GetContext(), {1, 1}, 1u);
2465 testing::MockRenderPass render_pass(GetContext(), target);
2466 auto position_result =
2467 geom->GetPositionBuffer(*GetContentContext(), entity, render_pass);
2468
2469 EXPECT_EQ(position_result.vertex_buffer.vertex_count, 0u);
2470
2471 EXPECT_EQ(geom->GetResultMode(), GeometryResult::Mode::kNormal);
2472}
2473
2474TEST_P(EntityTest, CanRenderEmptyPathsWithoutCrashing) {
2476
2477 EXPECT_TRUE(path.GetBounds().IsEmpty());
2478
2479 std::unique_ptr<Geometry> geom = Geometry::MakeFillPath(path);
2480 auto contents = std::make_shared<SolidColorContents>(geom.get());
2481 contents->SetColor(Color::Red());
2482
2483 Entity entity;
2484 entity.SetTransform(Matrix::MakeScale(GetContentScale()));
2485 entity.SetContents(contents);
2486
2487 ASSERT_TRUE(OpenPlaygroundHere(std::move(entity)));
2488}
2489
2490TEST_P(EntityTest, DrawSuperEllipse) {
2491 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2492 // UI state.
2493 static float alpha = 10;
2494 static float beta = 10;
2495 static float radius = 40;
2496 static int degree = 4;
2497 static Color color = Color::Red();
2498
2499 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2500 ImGui::SliderFloat("Alpha", &alpha, 0, 100);
2501 ImGui::SliderFloat("Beta", &beta, 0, 100);
2502 ImGui::SliderInt("Degreee", &degree, 1, 20);
2503 ImGui::SliderFloat("Radius", &radius, 0, 400);
2504 ImGui::ColorEdit4("Color", reinterpret_cast<float*>(&color));
2505 ImGui::End();
2506
2507 std::unique_ptr<SuperellipseGeometry> geom =
2508 std::make_unique<SuperellipseGeometry>(Point{400, 400}, radius, degree,
2509 alpha, beta);
2510 auto contents = std::make_shared<SolidColorContents>(geom.get());
2511 contents->SetColor(color);
2512
2513 Entity entity;
2514 entity.SetContents(contents);
2515
2516 return entity.Render(context, pass);
2517 };
2518
2519 ASSERT_TRUE(OpenPlaygroundHere(callback));
2520}
2521
2522TEST_P(EntityTest, DrawRoundSuperEllipse) {
2523 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2524 // UI state.
2525 static int style_index = 0;
2526 static Point center = {830, 830};
2527 static Point size = {600, 600};
2528 static bool horizontal_symmetry = true;
2529 static bool vertical_symmetry = true;
2530 static bool corner_symmetry = true;
2531
2532 const char* style_options[] = {"Fill", "Stroke"};
2533
2534 // Initially radius_tl[0] will be mirrored to all 8 values since all 3
2535 // symmetries are enabled.
2536 static std::array<float, 2> radius_tl = {200};
2537 static std::array<float, 2> radius_tr;
2538 static std::array<float, 2> radius_bl;
2539 static std::array<float, 2> radius_br;
2540
2541 auto AddRadiusControl = [](std::array<float, 2>& radii, const char* tb_name,
2542 const char* lr_name) {
2543 std::string name = "Radius";
2544 if (!horizontal_symmetry || !vertical_symmetry) {
2545 name += ":";
2546 }
2547 if (!vertical_symmetry) {
2548 name = name + " " + tb_name;
2549 }
2550 if (!horizontal_symmetry) {
2551 name = name + " " + lr_name;
2552 }
2553 if (corner_symmetry) {
2554 ImGui::SliderFloat(name.c_str(), radii.data(), 0, 1000);
2555 } else {
2556 ImGui::SliderFloat2(name.c_str(), radii.data(), 0, 1000);
2557 }
2558 };
2559
2560 if (corner_symmetry) {
2561 radius_tl[1] = radius_tl[0];
2562 radius_tr[1] = radius_tr[0];
2563 radius_bl[1] = radius_bl[0];
2564 radius_br[1] = radius_br[0];
2565 }
2566
2567 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2568 {
2569 ImGui::Combo("Style", &style_index, style_options,
2570 sizeof(style_options) / sizeof(char*));
2571 ImGui::SliderFloat2("Center", &center.x, 0, 1000);
2572 ImGui::SliderFloat2("Size", &size.x, 0, 1000);
2573 ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry);
2574 ImGui::Checkbox("Symmetry: Vertical", &vertical_symmetry);
2575 ImGui::Checkbox("Symmetry: Corners", &corner_symmetry);
2576 AddRadiusControl(radius_tl, "Top", "Left");
2577 if (!horizontal_symmetry) {
2578 AddRadiusControl(radius_tr, "Top", "Right");
2579 } else {
2580 radius_tr = radius_tl;
2581 }
2582 if (!vertical_symmetry) {
2583 AddRadiusControl(radius_bl, "Bottom", "Left");
2584 } else {
2585 radius_bl = radius_tl;
2586 }
2587 if (!horizontal_symmetry && !vertical_symmetry) {
2588 AddRadiusControl(radius_br, "Bottom", "Right");
2589 } else {
2590 if (horizontal_symmetry) {
2591 radius_br = radius_bl;
2592 } else {
2593 radius_br = radius_tr;
2594 }
2595 }
2596 }
2597
2598 ImGui::End();
2599
2600 RoundingRadii radii{
2601 .top_left = {radius_tl[0], radius_tl[1]},
2602 .top_right = {radius_tr[0], radius_tr[1]},
2603 .bottom_left = {radius_bl[0], radius_bl[1]},
2604 .bottom_right = {radius_br[0], radius_br[1]},
2605 };
2606
2608 Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2609
2611 std::unique_ptr<Geometry> geom;
2612 if (style_index == 0) {
2613 geom = std::make_unique<RoundSuperellipseGeometry>(
2614 Rect::MakeEllipseBounds(center, size * 0.5f), radii);
2615 } else {
2617 geom = Geometry::MakeStrokePath(path, {.width = 2.0f});
2618 }
2619
2620 auto contents = std::make_shared<SolidColorContents>(geom.get());
2621 contents->SetColor(Color::Red());
2622
2623 Entity entity;
2624 entity.SetContents(contents);
2625
2626 return entity.Render(context, pass);
2627 };
2628
2629 ASSERT_TRUE(OpenPlaygroundHere(callback));
2630}
2631
2632TEST_P(EntityTest, DrawRoundSuperEllipseWithLargeN) {
2633 // This playground shows the enlarged corner of a rounded superellipse to
2634 // compare pathing algorithm against the filling algorithm (benchmark) at very
2635 // large ratio.
2636 auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
2637 constexpr float corner_radius = 100.0;
2638
2639 // UI state.
2640 static float logarithm_of_ratio = 1.5; // ratio = size / corner_radius
2641
2642 float ratio = std::exp(logarithm_of_ratio);
2643
2644 float rect_size = corner_radius * ratio;
2645 Rect rect = Rect::MakeLTRB(0, 0, rect_size, rect_size);
2646 constexpr float screen_canvas_padding = 200.0f;
2647 constexpr float screen_canvas_size = 1000.0f;
2648
2649 // Scale so that "corner radius" is as long as half the canvas.
2650 float scale = screen_canvas_size / 2 / corner_radius;
2651
2652 ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
2653 {
2654 ImGui::SliderFloat("log(Ratio)", &logarithm_of_ratio, 1.0, 8.0);
2655 ImGui::LabelText("Ratio", "%.2g", static_cast<double>(ratio));
2656 ImGui::Text(" where Ratio = RectSize / CornerRadius");
2657 }
2658 ImGui::End();
2659
2660 auto top_right = Vector2(screen_canvas_size * 1.3f, screen_canvas_padding);
2661 auto transform = Matrix::MakeTranslation(top_right) *
2662 Matrix::MakeScale(Vector2(scale, scale)) *
2663 Matrix::MakeTranslation(Vector2(-rect_size, 0));
2664 bool success = true;
2665
2666 auto fill_geom =
2667 std::make_unique<RoundSuperellipseGeometry>(rect, corner_radius);
2668 // Fill
2669 {
2670 auto contents = std::make_shared<SolidColorContents>(fill_geom.get());
2671 contents->SetColor(Color::Red());
2672
2673 Entity entity;
2674 entity.SetContents(contents);
2675 entity.SetTransform(transform);
2676
2677 success = success && entity.Render(context, pass);
2678 }
2679
2680 // Stroke
2682 RoundSuperellipse::MakeRectRadius(rect, corner_radius));
2683 auto stroke_geom = Geometry::MakeStrokePath(path, {.width = 2 / scale});
2684 {
2685 auto contents = std::make_shared<SolidColorContents>(stroke_geom.get());
2686 contents->SetColor(Color::Blue());
2687
2688 Entity entity;
2689 entity.SetContents(contents);
2690 entity.SetTransform(transform);
2691
2692 success = success && entity.Render(context, pass);
2693 }
2694
2695 // Draw a ruler to show the length in portion of rect size.
2696 auto screen_top_right =
2697 Matrix::MakeScale(GetContentScale()).Invert() * top_right;
2698 constexpr float font_size = 13.0f;
2699 for (int i = -1; i < 100; i++) {
2700 float screen_offset_y = static_cast<float>(i) * 20.0f;
2701 std::string label;
2702 if (i == -1) {
2703 label = "Ruler: (in portion of rect size)";
2704 } else if (i == 0) {
2705 label = "- 0.0";
2706 } else {
2707 float portion_of_rect =
2708 screen_offset_y * GetContentScale().y / scale / rect_size;
2709 label = std::format("- {:.2g}", portion_of_rect);
2710 }
2711 ImGui::GetBackgroundDrawList()->AddText(
2712 nullptr, font_size,
2713 // Draw the ruler at around the flat part of the curve, which is
2714 // somewhere to the left of the top right corner.
2715 //
2716 // Offset vertically by font_size/2 so that the hyphen aligns with the
2717 // top of shape.
2718 {screen_top_right.x - 500,
2719 screen_top_right.y + screen_offset_y - font_size / 2},
2720 IM_COL32_WHITE, label.c_str());
2721 }
2722 return success;
2723 };
2724
2725 ASSERT_TRUE(OpenPlaygroundHere(callback));
2726}
2727
2728TEST_P(EntityTest, CanDrawRoundSuperEllipseWithTinyRadius) {
2729 // Regression test for https://github.com/flutter/flutter/issues/176894
2730 // Verify that a radius marginally below the minimum threshold can be
2731 // processed safely. The expectation is that the rounded corners degenerate
2732 // into sharp corners (four corner points) and that no NaNs or crashes occur.
2734 Rect::MakeLTRB(200, 200, 300, 300), 0.5 * kEhCloseEnough);
2735
2736 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2737 Entity entity;
2738
2739 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2740
2742 content_context.GetContext()->GetResourceAllocator());
2743
2744 auto render_target = allocator.CreateOffscreen(
2745 *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2746 auto pass = cmd_buffer->CreateRenderPass(render_target);
2747
2748 GeometryResult result =
2749 geom->GetPositionBuffer(content_context, entity, *pass);
2750
2751 EXPECT_EQ(result.vertex_buffer.vertex_count, 4u);
2752 Point* written_data = reinterpret_cast<Point*>(
2755
2756 std::vector<Point> expected = {Point(300.0, 200.0), Point(300.0, 300.0),
2757 Point(200.0, 200.0), Point(200.0, 300.0)};
2758
2759 for (size_t i = 0; i < expected.size(); i++) {
2760 const Point& point = written_data[i];
2761 EXPECT_NEAR(point.x, expected[i].x, 0.1);
2762 EXPECT_NEAR(point.y, expected[i].y, 0.1);
2763 }
2764}
2765
2766TEST_P(EntityTest, CanDrawRoundSuperEllipseWithJustEnoughRadius) {
2767 // Regression test for https://github.com/flutter/flutter/issues/176894
2768 // Verify that a radius marginally above the minimum threshold can be
2769 // processed safely. The expectation is that the rounded corners are
2770 // drawn as rounded and that no NaNs or crashes occur.
2772 Rect::MakeLTRB(200, 200, 300, 300), 1.1 * kEhCloseEnough);
2773
2774 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2775 Entity entity;
2776
2777 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2778
2780 content_context.GetContext()->GetResourceAllocator());
2781
2782 auto render_target = allocator.CreateOffscreen(
2783 *content_context.GetContext(), /*size=*/{500, 500}, /*mip_count=*/1);
2784 auto pass = cmd_buffer->CreateRenderPass(render_target);
2785
2786 GeometryResult result =
2787 geom->GetPositionBuffer(content_context, entity, *pass);
2788
2789 EXPECT_EQ(result.vertex_buffer.vertex_count, 200u);
2790 Point* written_data = reinterpret_cast<Point*>(
2793
2794 std::vector<Point> expected_head = {Point(250.0, 200.0), Point(299.9, 200.0),
2795 Point(200.1, 200.0), Point(299.9, 200.0)};
2796
2797 for (size_t i = 0; i < expected_head.size(); i++) {
2798 const Point& point = written_data[i];
2799 EXPECT_NEAR(point.x, expected_head[i].x, 0.1);
2800 EXPECT_NEAR(point.y, expected_head[i].y, 0.1);
2801 }
2802}
2803
2804TEST_P(EntityTest, SolidColorApplyColorFilter) {
2805 auto geom = Geometry::MakeCover();
2806 auto contents = SolidColorContents(geom.get());
2807 contents.SetColor(Color::CornflowerBlue().WithAlpha(0.75));
2808 auto result = contents.ApplyColorFilter([](const Color& color) {
2809 return color.Blend(Color::LimeGreen().WithAlpha(0.75), BlendMode::kScreen);
2810 });
2811 ASSERT_TRUE(result);
2812 ASSERT_COLOR_NEAR(contents.GetColor(),
2813 Color(0.424452, 0.828743, 0.79105, 0.9375));
2814}
2815
2816#define APPLY_COLOR_FILTER_GRADIENT_TEST(name) \
2817 TEST_P(EntityTest, name##GradientApplyColorFilter) { \
2818 auto geom = Geometry::MakeCover(); \
2819 auto contents = name##GradientContents(geom.get()); \
2820 contents.SetColors({Color::CornflowerBlue().WithAlpha(0.75)}); \
2821 auto result = contents.ApplyColorFilter([](const Color& color) { \
2822 return color.Blend(Color::LimeGreen().WithAlpha(0.75), \
2823 BlendMode::kScreen); \
2824 }); \
2825 ASSERT_TRUE(result); \
2826 \
2827 std::vector<Color> expected = {Color(0.433247, 0.879523, 0.825324, 0.75)}; \
2828 ASSERT_COLORS_NEAR(contents.GetColors(), expected); \
2829 }
2830
2835
2836TEST_P(EntityTest, GiantStrokePathAllocation) {
2837 flutter::DlPathBuilder builder;
2838 for (int i = 0; i < 10000; i++) {
2839 builder.LineTo(Point(i, i));
2840 }
2841 flutter::DlPath path = builder.TakePath();
2842 auto geom = Geometry::MakeStrokePath(path, {.width = 10.0f});
2843
2844 ContentContext content_context(GetContext(), /*typographer_context=*/nullptr);
2845 Entity entity;
2846
2847 auto cmd_buffer = content_context.GetContext()->CreateCommandBuffer();
2848
2850 content_context.GetContext()->GetResourceAllocator());
2851
2852 auto render_target = allocator.CreateOffscreen(
2853 *content_context.GetContext(), /*size=*/{10, 10}, /*mip_count=*/1);
2854 auto pass = cmd_buffer->CreateRenderPass(render_target);
2855
2856 GeometryResult result =
2857 geom->GetPositionBuffer(content_context, entity, *pass);
2858
2859 // Validate the buffer data overflowed the small buffer
2860 EXPECT_GT(result.vertex_buffer.vertex_count, kPointArenaSize);
2861
2862 // Validate that there are no uninitialized points near the gap.
2863 Point* written_data = reinterpret_cast<Point*>(
2866
2867 std::vector<Point> expected = {
2868 Point(2043.46, 2050.54), //
2869 Point(2050.54, 2043.46), //
2870 Point(2044.46, 2051.54), //
2871 Point(2051.54, 2044.46), //
2872 Point(2045.46, 2052.54) //
2873 };
2874
2875 Point point = written_data[kPointArenaSize - 2];
2876 EXPECT_NEAR(point.x, expected[0].x, 0.1);
2877 EXPECT_NEAR(point.y, expected[0].y, 0.1);
2878
2879 point = written_data[kPointArenaSize - 1];
2880 EXPECT_NEAR(point.x, expected[1].x, 0.1);
2881 EXPECT_NEAR(point.y, expected[1].y, 0.1);
2882
2883 point = written_data[kPointArenaSize];
2884 EXPECT_NEAR(point.x, expected[2].x, 0.1);
2885 EXPECT_NEAR(point.y, expected[2].y, 0.1);
2886
2887 point = written_data[kPointArenaSize + 1];
2888 EXPECT_NEAR(point.x, expected[3].x, 0.1);
2889 EXPECT_NEAR(point.y, expected[3].y, 0.1);
2890
2891 point = written_data[kPointArenaSize + 2];
2892 EXPECT_NEAR(point.x, expected[4].x, 0.1);
2893 EXPECT_NEAR(point.y, expected[4].y, 0.1);
2894}
2895
2897 public:
2899 : DeviceBuffer(desc), storage_(desc.size) {}
2900
2901 bool SetLabel(std::string_view label) override { return true; }
2902 bool SetLabel(std::string_view label, Range range) override { return true; }
2903 bool OnCopyHostBuffer(const uint8_t* source,
2904 Range source_range,
2905 size_t offset) {
2906 return true;
2907 }
2908
2909 uint8_t* OnGetContents() const override {
2910 return const_cast<uint8_t*>(storage_.data());
2911 }
2912
2913 void Flush(std::optional<Range> range) const override {
2914 flush_called_ = true;
2915 }
2916
2917 bool flush_called() const { return flush_called_; }
2918
2919 private:
2920 std::vector<uint8_t> storage_;
2921 mutable bool flush_called_ = false;
2922};
2923
2925 public:
2927 return ISize(1024, 1024);
2928 };
2929
2930 std::shared_ptr<DeviceBuffer> OnCreateBuffer(
2931 const DeviceBufferDescriptor& desc) override {
2932 return std::make_shared<FlushTestDeviceBuffer>(desc);
2933 };
2934
2935 std::shared_ptr<Texture> OnCreateTexture(const TextureDescriptor& desc,
2936 bool threadsafe) override {
2937 return nullptr;
2938 }
2939};
2940
2942 public:
2944 const std::shared_ptr<Context>& context,
2945 const std::shared_ptr<TypographerContext>& typographer_context,
2946 const std::shared_ptr<Allocator>& allocator)
2947 : ContentContext(context, typographer_context) {
2949 allocator, context->GetIdleWaiter(),
2950 context->GetCapabilities()->GetMinimumUniformAlignment()));
2952 allocator, context->GetIdleWaiter(),
2953 context->GetCapabilities()->GetMinimumUniformAlignment()));
2954 }
2955};
2956
2957TEST_P(EntityTest, RoundSuperellipseGetPositionBufferFlushes) {
2959 testing::MockRenderPass mock_pass(GetContext(), target);
2960
2961 auto content_context = std::make_shared<FlushTestContentContext>(
2962 GetContext(), GetTypographerContext(),
2963 std::make_shared<FlushTestAllocator>());
2964 auto geometry =
2966 auto result = geometry->GetPositionBuffer(*content_context, {}, mock_pass);
2967
2968 auto device_buffer = reinterpret_cast<const FlushTestDeviceBuffer*>(
2969 result.vertex_buffer.vertex_buffer.GetBuffer());
2970 EXPECT_TRUE(device_buffer->flush_called());
2971}
2972
2973} // namespace testing
2974} // namespace impeller
2975
2976// 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)
HostBuffer & GetTransientsDataBuffer() const
Retrieve the current host buffer for transient storage of other non-index data.
void SetTransientsDataBuffer(std::shared_ptr< HostBuffer > host_buffer)
PipelineRef GetSolidFillPipeline(ContentContextOptions opts) const
void SetTransientsIndexesBuffer(std::shared_ptr< HostBuffer > host_buffer)
HostBuffer & GetTransientsIndexesBuffer() const
Retrieve the current host buffer for transient storage of indexes used for indexed draws.
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
virtual std::shared_ptr< CommandQueue > GetCommandQueue() const =0
Return the graphics queue for submitting command buffers.
virtual std::shared_ptr< CommandBuffer > CreateCommandBuffer() const =0
Create a new command buffer. Command buffers can be used to encode graphics, blit,...
virtual std::shared_ptr< Allocator > GetResourceAllocator() const =0
Returns the allocator used to create textures and buffers on the device.
static BufferView AsBufferView(std::shared_ptr< DeviceBuffer > buffer)
Create a buffer view of this entire buffer.
virtual uint8_t * OnGetContents() const =0
void SetTransform(const Matrix &transform)
Set the global transform matrix for this Entity.
Definition entity.cc:62
std::optional< Rect > GetCoverage() const
Definition entity.cc:66
BlendMode GetBlendMode() const
Definition entity.cc:102
void SetContents(std::shared_ptr< Contents > contents)
Definition entity.cc:74
void SetBlendMode(BlendMode blend_mode)
Definition entity.cc:98
bool Render(const ContentContext &renderer, RenderPass &parent_pass) const
Definition entity.cc:145
const Matrix & GetTransform() const
Get the global transform matrix for this Entity.
Definition entity.cc:46
static constexpr BlendMode kLastPipelineBlendMode
Definition entity.h:28
static std::shared_ptr< FilterContents > MakeGaussianBlur(const FilterInput::Ref &input, Sigma sigma_x, Sigma sigma_y, Entity::TileMode tile_mode=Entity::TileMode::kDecal, std::optional< Rect > bounds=std::nullopt, BlurStyle mask_blur_style=BlurStyle::kNormal, const Geometry *mask_geometry=nullptr)
@ kNormal
Blurred inside and outside.
@ kOuter
Nothing inside, blurred outside.
@ kInner
Blurred inside, nothing outside.
@ kSolid
Solid inside, blurred outside.
static std::shared_ptr< FilterContents > MakeMorphology(FilterInput::Ref input, Radius radius_x, Radius radius_y, MorphType morph_type)
static std::shared_ptr< FilterContents > MakeBorderMaskBlur(FilterInput::Ref input, Sigma sigma_x, Sigma sigma_y, BlurStyle blur_style=BlurStyle::kNormal)
static std::shared_ptr< FilterContents > MakeYUVToRGBFilter(std::shared_ptr< Texture > y_texture, std::shared_ptr< Texture > uv_texture, YUVColorSpace yuv_color_space)
static FilterInput::Ref Make(Variant input, bool msaa_enabled=true)
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)
BufferView EmplaceUniform(const UniformType &uniform)
Emplace uniform data onto the host buffer. Ensure that backend specific uniform alignment requirement...
Definition host_buffer.h:47
void SetTileMode(Entity::TileMode tile_mode)
void SetColors(std::vector< Color > colors)
bool IsOpaque(const Matrix &transform) const override
Whether this Contents only emits opaque source colors from the fragment stage. This value does not ac...
A geometry class specialized for Canvas::DrawPoints.
std::optional< Rect > GetCoverage(const Matrix &transform) const override
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:122
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)
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