From 1afb79fa193a5b9bb132789fa8b12c2565858bcb Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Wed, 21 Aug 2019 18:38:28 -0700 Subject: [PATCH] Account for root surface transformation on the surfaces managed by the external view embedder. The earlier design speculated that embedders could affect the same transformations on the layers post engine compositor presentation but before final composition. However, the linked issue points out that this design is not suitable for use with hardware overlay planes. When rendering to the same, to affect the transformation before composition, embedders would have to render to an off-screen render target and then apply the transformation before presentation. This patch negates the need for that off-screen render pass. To be clear, the previous architecture is still fully viable. Embedders still have full control over layer transformations before composition. This is an optimization for the hardware overlay planes use-case. Fixes b/139758641 --- ci/licenses_golden/licenses_flutter | 7 +- flow/BUILD.gn | 2 - flow/debug_print.cc | 84 -- flow/debug_print.h | 29 - lib/ui/painting/image_decoder_unittests.cc | 3 +- shell/common/shell_test.cc | 3 +- shell/platform/embedder/BUILD.gn | 6 + shell/platform/embedder/embedder.cc | 33 +- shell/platform/embedder/embedder.h | 8 + .../embedder_external_view_embedder.cc | 112 ++- .../embedder_external_view_embedder.h | 16 + .../platform/embedder/fixtures/compositor.png | Bin 1707 -> 2614 bytes .../compositor_root_surface_xformation.png | Bin 0 -> 9485 bytes .../embedder/fixtures/compositor_software.png | Bin 1709 -> 2616 bytes shell/platform/embedder/fixtures/gradient.png | Bin 0 -> 33551 bytes .../embedder/fixtures/gradient_xform.png | Bin 0 -> 82500 bytes shell/platform/embedder/fixtures/main.dart | 104 +++ .../scene_without_custom_compositor.png | Bin 0 -> 2627 bytes ...e_without_custom_compositor_with_xform.png | Bin 0 -> 9532 bytes .../embedder/tests/embedder_a11y_unittests.cc | 1 + .../embedder/tests/embedder_assertions.h | 14 + .../embedder/tests/embedder_config_builder.cc | 18 +- .../embedder/tests/embedder_config_builder.h | 4 +- .../tests/embedder_test_compositor.cc | 45 +- .../embedder/tests/embedder_test_compositor.h | 3 +- .../embedder/tests/embedder_test_context.cc | 56 +- .../embedder/tests/embedder_test_context.h | 15 +- .../embedder/tests/embedder_unittests.cc | 741 ++++++++++++++++-- testing/BUILD.gn | 15 +- testing/assertions_skia.h | 79 ++ testing/test_gl_surface.cc | 64 +- testing/test_gl_surface.h | 5 +- 32 files changed, 1175 insertions(+), 292 deletions(-) delete mode 100644 flow/debug_print.cc delete mode 100644 flow/debug_print.h create mode 100644 shell/platform/embedder/fixtures/compositor_root_surface_xformation.png create mode 100644 shell/platform/embedder/fixtures/gradient.png create mode 100644 shell/platform/embedder/fixtures/gradient_xform.png create mode 100644 shell/platform/embedder/fixtures/scene_without_custom_compositor.png create mode 100644 shell/platform/embedder/fixtures/scene_without_custom_compositor_with_xform.png create mode 100644 testing/assertions_skia.h diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 6d3f396ad616f..01528ab325b36 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -24,8 +24,6 @@ FILE: ../../../flutter/common/task_runners.cc FILE: ../../../flutter/common/task_runners.h FILE: ../../../flutter/flow/compositor_context.cc FILE: ../../../flutter/flow/compositor_context.h -FILE: ../../../flutter/flow/debug_print.cc -FILE: ../../../flutter/flow/debug_print.h FILE: ../../../flutter/flow/embedded_views.cc FILE: ../../../flutter/flow/embedded_views.h FILE: ../../../flutter/flow/instrumentation.cc @@ -847,10 +845,15 @@ FILE: ../../../flutter/shell/platform/embedder/embedder_task_runner.h FILE: ../../../flutter/shell/platform/embedder/embedder_thread_host.cc FILE: ../../../flutter/shell/platform/embedder/embedder_thread_host.h FILE: ../../../flutter/shell/platform/embedder/fixtures/compositor.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/compositor_root_surface_xformation.png FILE: ../../../flutter/shell/platform/embedder/fixtures/compositor_software.png FILE: ../../../flutter/shell/platform/embedder/fixtures/compositor_with_platform_layer_on_bottom.png FILE: ../../../flutter/shell/platform/embedder/fixtures/compositor_with_root_layer_only.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/gradient.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/gradient_xform.png FILE: ../../../flutter/shell/platform/embedder/fixtures/main.dart +FILE: ../../../flutter/shell/platform/embedder/fixtures/scene_without_custom_compositor.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/scene_without_custom_compositor_with_xform.png FILE: ../../../flutter/shell/platform/embedder/platform_view_embedder.cc FILE: ../../../flutter/shell/platform/embedder/platform_view_embedder.h FILE: ../../../flutter/shell/platform/embedder/vsync_waiter_embedder.cc diff --git a/flow/BUILD.gn b/flow/BUILD.gn index 133cddca4fcd7..90975c05e6096 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -12,8 +12,6 @@ source_set("flow") { sources = [ "compositor_context.cc", "compositor_context.h", - "debug_print.cc", - "debug_print.h", "embedded_views.cc", "embedded_views.h", "instrumentation.cc", diff --git a/flow/debug_print.cc b/flow/debug_print.cc deleted file mode 100644 index 965c3c2c78614..0000000000000 --- a/flow/debug_print.cc +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "flutter/flow/debug_print.h" - -#include - -#include "third_party/skia/include/core/SkString.h" - -std::ostream& operator<<(std::ostream& os, - const flutter::MatrixDecomposition& m) { - if (!m.IsValid()) { - os << "Invalid Matrix!" << std::endl; - return os; - } - - os << "Translation (x, y, z): " << m.translation() << std::endl; - os << "Scale (z, y, z): " << m.scale() << std::endl; - os << "Shear (zy, yz, zx): " << m.shear() << std::endl; - os << "Perspective (x, y, z, w): " << m.perspective() << std::endl; - os << "Rotation (x, y, z, w): " << m.rotation() << std::endl; - - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkMatrix& m) { - SkString string; - string.printf("[%8.4f %8.4f %8.4f][%8.4f %8.4f %8.4f][%8.4f %8.4f %8.4f]", - m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8]); - os << string.c_str(); - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkMatrix44& m) { - os << m.get(0, 0) << ", " << m.get(0, 1) << ", " << m.get(0, 2) << ", " - << m.get(0, 3) << std::endl; - os << m.get(1, 0) << ", " << m.get(1, 1) << ", " << m.get(1, 2) << ", " - << m.get(1, 3) << std::endl; - os << m.get(2, 0) << ", " << m.get(2, 1) << ", " << m.get(2, 2) << ", " - << m.get(2, 3) << std::endl; - os << m.get(3, 0) << ", " << m.get(3, 1) << ", " << m.get(3, 2) << ", " - << m.get(3, 3); - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkVector3& v) { - os << v.x() << ", " << v.y() << ", " << v.z(); - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkVector4& v) { - os << v.fData[0] << ", " << v.fData[1] << ", " << v.fData[2] << ", " - << v.fData[3]; - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkRect& r) { - os << "LTRB: " << r.fLeft << ", " << r.fTop << ", " << r.fRight << ", " - << r.fBottom; - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkRRect& r) { - os << "LTRB: " << r.rect().fLeft << ", " << r.rect().fTop << ", " - << r.rect().fRight << ", " << r.rect().fBottom; - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkPoint& r) { - os << "XY: " << r.fX << ", " << r.fY; - return os; -} - -std::ostream& operator<<(std::ostream& os, - const flutter::PictureRasterCacheKey& k) { - os << "Picture: " << k.id() << " matrix: " << k.matrix(); - return os; -} - -std::ostream& operator<<(std::ostream& os, const SkISize& size) { - os << size.width() << ", " << size.height(); - return os; -} diff --git a/flow/debug_print.h b/flow/debug_print.h deleted file mode 100644 index f8e0d239c8c86..0000000000000 --- a/flow/debug_print.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_FLOW_DEBUG_PRINT_H_ -#define FLUTTER_FLOW_DEBUG_PRINT_H_ - -#include "flutter/flow/matrix_decomposition.h" -#include "flutter/flow/raster_cache_key.h" -#include "flutter/fml/macros.h" -#include "third_party/skia/include/core/SkMatrix.h" -#include "third_party/skia/include/core/SkMatrix44.h" -#include "third_party/skia/include/core/SkPoint3.h" -#include "third_party/skia/include/core/SkRRect.h" - -#define DEF_PRINTER(x) std::ostream& operator<<(std::ostream&, const x&); - -DEF_PRINTER(flutter::MatrixDecomposition); -DEF_PRINTER(flutter::PictureRasterCacheKey); -DEF_PRINTER(SkISize); -DEF_PRINTER(SkMatrix); -DEF_PRINTER(SkMatrix44); -DEF_PRINTER(SkPoint); -DEF_PRINTER(SkRect); -DEF_PRINTER(SkRRect); -DEF_PRINTER(SkVector3); -DEF_PRINTER(SkVector4); - -#endif // FLUTTER_FLOW_DEBUG_PRINT_H_ diff --git a/lib/ui/painting/image_decoder_unittests.cc b/lib/ui/painting/image_decoder_unittests.cc index 7045a248cd979..8786150d0a864 100644 --- a/lib/ui/painting/image_decoder_unittests.cc +++ b/lib/ui/painting/image_decoder_unittests.cc @@ -19,7 +19,8 @@ class TestIOManager final : public IOManager { public: TestIOManager(fml::RefPtr task_runner, bool has_gpu_context = true) - : gl_context_(has_gpu_context ? gl_surface_.CreateGrContext() : nullptr), + : gl_surface_(SkISize::Make(1, 1)), + gl_context_(has_gpu_context ? gl_surface_.CreateGrContext() : nullptr), weak_gl_context_factory_( has_gpu_context ? std::make_unique>( gl_context_.get()) diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index 87f41b5a8fb3c..3ede4058be9a6 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -234,7 +234,8 @@ void ShellTest::AddNativeCallback(std::string name, ShellTestPlatformView::ShellTestPlatformView(PlatformView::Delegate& delegate, TaskRunners task_runners) - : PlatformView(delegate, std::move(task_runners)) {} + : PlatformView(delegate, std::move(task_runners)), + gl_surface_(SkISize::Make(800, 600)) {} ShellTestPlatformView::~ShellTestPlatformView() = default; diff --git a/shell/platform/embedder/BUILD.gn b/shell/platform/embedder/BUILD.gn index 7c7095757687e..b1fd6ab492b55 100644 --- a/shell/platform/embedder/BUILD.gn +++ b/shell/platform/embedder/BUILD.gn @@ -85,9 +85,14 @@ test_fixtures("fixtures") { dart_main = "fixtures/main.dart" fixtures = [ "fixtures/compositor.png", + "fixtures/gradient.png", + "fixtures/gradient_xform.png", "fixtures/compositor_software.png", "fixtures/compositor_with_platform_layer_on_bottom.png", "fixtures/compositor_with_root_layer_only.png", + "fixtures/compositor_root_surface_xformation.png", + "fixtures/scene_without_custom_compositor.png", + "fixtures/scene_without_custom_compositor_with_xform.png", ] } @@ -119,6 +124,7 @@ if (current_toolchain == host_toolchain) { "$flutter_root/runtime", "$flutter_root/testing:dart", "$flutter_root/testing:opengl", + "$flutter_root/testing:skia", "//third_party/skia", "//third_party/tonic", ] diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index bc412d903bdc6..43599c52fc1ec 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -171,6 +171,13 @@ InferOpenGLPlatformViewCreationCallback( transformation.pers2 // ); }; + + // If there is an external view embedder, ask it to apply the surface + // transformation to its surfaces as well. + if (external_view_embedder) { + external_view_embedder->SetSurfaceTransformationCallback( + gl_surface_transformation_callback); + } } flutter::GPUSurfaceGLDelegate::GLProcResolver gl_proc_resolver = nullptr; @@ -296,13 +303,13 @@ static sk_sp MakeSkSurfaceFromBackingStore( SkSurfaceProps::InitType::kLegacyFontHost_InitType); auto surface = SkSurface::MakeFromBackendTexture( - context, // context - backend_texture, // back-end texture - kTopLeft_GrSurfaceOrigin, // surface origin - 1, // sample count - kN32_SkColorType, // color type - SkColorSpace::MakeSRGB(), // color space - &surface_properties, // surface properties + context, // context + backend_texture, // back-end texture + kBottomLeft_GrSurfaceOrigin, // surface origin + 1, // sample count + kN32_SkColorType, // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties, // surface properties static_cast( texture->destruction_callback), // release proc texture->user_data // release context @@ -337,12 +344,12 @@ static sk_sp MakeSkSurfaceFromBackingStore( SkSurfaceProps::InitType::kLegacyFontHost_InitType); auto surface = SkSurface::MakeFromBackendRenderTarget( - context, // context - backend_render_target, // backend render target - kTopLeft_GrSurfaceOrigin, // surface origin - kN32_SkColorType, // color type - SkColorSpace::MakeSRGB(), // color space - &surface_properties, // surface properties + context, // context + backend_render_target, // backend render target + kBottomLeft_GrSurfaceOrigin, // surface origin + kN32_SkColorType, // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties, // surface properties static_cast( framebuffer->destruction_callback), // release proc framebuffer->user_data // release context diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 0f889393f5273..ee03b66cf10ef 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -282,6 +282,14 @@ typedef struct { bool fbo_reset_after_present; /// The transformation to apply to the render target before any rendering /// operations. This callback is optional. + /// @attention When using a custom compositor, the layer offset and sizes + /// will be affected by this transformation. It will be + /// embedder responsibility to render contents at the + /// transformed offset and size. This is useful for embedders + /// that want to render transformed contents directly into + /// hardware overlay planes without having to apply extra + /// transformations to layer contents (which may necessitate + /// an expensive off-screen render pass). TransformationCallback surface_transformation; ProcResolver gl_proc_resolver; /// When the embedder specifies that a texture has a frame available, the diff --git a/shell/platform/embedder/embedder_external_view_embedder.cc b/shell/platform/embedder/embedder_external_view_embedder.cc index 064468f85c435..0ef6584541b25 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.cc +++ b/shell/platform/embedder/embedder_external_view_embedder.cc @@ -21,6 +21,19 @@ EmbedderExternalViewEmbedder::EmbedderExternalViewEmbedder( EmbedderExternalViewEmbedder::~EmbedderExternalViewEmbedder() = default; +void EmbedderExternalViewEmbedder::SetSurfaceTransformationCallback( + SurfaceTransformationCallback surface_transformation_callback) { + surface_transformation_callback_ = surface_transformation_callback; +} + +SkMatrix EmbedderExternalViewEmbedder::GetSurfaceTransformation() const { + if (!surface_transformation_callback_) { + return SkMatrix{}; + } + + return surface_transformation_callback_(); +} + void EmbedderExternalViewEmbedder::Reset() { pending_recorders_.clear(); pending_canvas_spies_.clear(); @@ -33,22 +46,34 @@ void EmbedderExternalViewEmbedder::CancelFrame() { Reset(); } -static FlutterBackingStoreConfig MakeBackingStoreConfig(const SkISize& size) { +static FlutterBackingStoreConfig MakeBackingStoreConfig( + const SkISize& backing_store_size) { FlutterBackingStoreConfig config = {}; config.struct_size = sizeof(config); - config.size.width = size.width(); - config.size.height = size.height(); + config.size.width = backing_store_size.width(); + config.size.height = backing_store_size.height(); return config; } +static SkISize TransformedSurfaceSize(const SkISize& size, + const SkMatrix& transformation) { + const auto source_rect = SkRect::MakeWH(size.width(), size.height()); + const auto transformed_rect = transformation.mapRect(source_rect); + return SkISize::Make(transformed_rect.width(), transformed_rect.height()); +} + // |ExternalViewEmbedder| void EmbedderExternalViewEmbedder::BeginFrame(SkISize frame_size, GrContext* context) { Reset(); pending_frame_size_ = frame_size; + pending_surface_transformation_ = GetSurfaceTransformation(); + + const auto surface_size = TransformedSurfaceSize( + pending_frame_size_, pending_surface_transformation_); // Decide if we want to discard the previous root render target. if (root_render_target_) { @@ -61,7 +86,7 @@ void EmbedderExternalViewEmbedder::BeginFrame(SkISize frame_size, } else { auto last_surface_size = SkISize::Make(surface->width(), surface->height()); - if (pending_frame_size_ != last_surface_size) { + if (surface_size != last_surface_size) { root_render_target_ = nullptr; } } @@ -72,7 +97,19 @@ void EmbedderExternalViewEmbedder::BeginFrame(SkISize frame_size, // canvas. if (!root_render_target_) { root_render_target_ = create_render_target_callback_( - context, MakeBackingStoreConfig(pending_frame_size_)); + context, MakeBackingStoreConfig(surface_size)); + } + + // Install the root surface transformation on the root canvas at the beginning + // of each frame. + if (root_render_target_) { + auto surface = root_render_target_->GetRenderSurface(); + if (surface) { + auto canvas = surface->getCanvas(); + if (canvas) { + canvas->setMatrix(pending_surface_transformation_); + } + } } } @@ -115,19 +152,26 @@ SkCanvas* EmbedderExternalViewEmbedder::CompositeEmbeddedView(int view_id) { return found->second->GetSpyingCanvas(); } -static FlutterLayer MakeLayer(const SkISize& frame_size, - const FlutterBackingStore* store) { +static FlutterLayer MakeBackingStoreLayer( + const SkISize& frame_size, + const FlutterBackingStore* store, + const SkMatrix& surface_transformation) { FlutterLayer layer = {}; layer.struct_size = sizeof(layer); layer.type = kFlutterLayerContentTypeBackingStore; layer.backing_store = store; - layer.offset.x = 0.0; - layer.offset.y = 0.0; + const auto layer_bounds = + SkRect::MakeWH(frame_size.width(), frame_size.height()); + + const auto transformed_layer_bounds = + surface_transformation.mapRect(layer_bounds); - layer.size.width = frame_size.width(); - layer.size.height = frame_size.height(); + layer.offset.x = transformed_layer_bounds.x(); + layer.offset.y = transformed_layer_bounds.y(); + layer.size.width = transformed_layer_bounds.width(); + layer.size.height = transformed_layer_bounds.height(); return layer; } @@ -143,19 +187,29 @@ static FlutterPlatformView MakePlatformView( return view; } -static FlutterLayer MakeLayer(const EmbeddedViewParams& params, - const FlutterPlatformView& platform_view) { +static FlutterLayer MakePlatformViewLayer( + const EmbeddedViewParams& params, + const FlutterPlatformView& platform_view, + const SkMatrix& surface_transformation) { FlutterLayer layer = {}; layer.struct_size = sizeof(layer); layer.type = kFlutterLayerContentTypePlatformView; layer.platform_view = &platform_view; - layer.offset.x = params.offsetPixels.x(); - layer.offset.y = params.offsetPixels.y(); + const auto layer_bounds = SkRect::MakeXYWH(params.offsetPixels.x(), // + params.offsetPixels.y(), // + params.sizePoints.width(), // + params.sizePoints.height() // + ); - layer.size.width = params.sizePoints.width(); - layer.size.height = params.sizePoints.height(); + const auto transformed_layer_bounds = + surface_transformation.mapRect(layer_bounds); + + layer.offset.x = transformed_layer_bounds.x(); + layer.offset.y = transformed_layer_bounds.y(); + layer.size.width = transformed_layer_bounds.width(); + layer.size.height = transformed_layer_bounds.height(); return layer; } @@ -180,13 +234,14 @@ bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { { // The root surface is expressed as a layer. - EmbeddedViewParams params; - params.offsetPixels = SkPoint::Make(0, 0); - params.sizePoints = pending_frame_size_; - presented_layers.push_back( - MakeLayer(pending_frame_size_, root_render_target_->GetBackingStore())); + presented_layers.push_back(MakeBackingStoreLayer( + pending_frame_size_, root_render_target_->GetBackingStore(), + pending_surface_transformation_)); } + const auto surface_size = TransformedSurfaceSize( + pending_frame_size_, pending_surface_transformation_); + for (const auto& view_id : composition_order_) { FML_DCHECK(pending_recorders_.count(view_id) == 1); FML_DCHECK(pending_canvas_spies_.count(view_id) == 1); @@ -208,15 +263,16 @@ bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { // struct. It is safe to deallocate when the embedder callback is done. presented_platform_views[view_id] = MakePlatformView(view_id); presented_layers.push_back( - MakeLayer(params, presented_platform_views.at(view_id))); + MakePlatformViewLayer(params, presented_platform_views.at(view_id), + pending_surface_transformation_)); if (!pending_canvas_spies_.at(view_id)->DidDrawIntoCanvas()) { // Nothing was drawn into the overlay canvas, we don't need to composite // it. continue; } - const auto backing_store_config = - MakeBackingStoreConfig(pending_frame_size_); + + const auto backing_store_config = MakeBackingStoreConfig(surface_size); RegistryKey registry_key(view_id, backing_store_config); @@ -249,13 +305,15 @@ bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { return false; } + render_canvas->setMatrix(pending_surface_transformation_); render_canvas->clear(SK_ColorTRANSPARENT); render_canvas->drawPicture(picture); render_canvas->flush(); // Indicate a layer for the backing store containing contents rendered by // Flutter. - presented_layers.push_back( - MakeLayer(pending_frame_size_, render_target->GetBackingStore())); + presented_layers.push_back(MakeBackingStoreLayer( + pending_frame_size_, render_target->GetBackingStore(), + pending_surface_transformation_)); } { diff --git a/shell/platform/embedder/embedder_external_view_embedder.h b/shell/platform/embedder/embedder_external_view_embedder.h index d34ba2e1f00bb..65b001cee040e 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.h +++ b/shell/platform/embedder/embedder_external_view_embedder.h @@ -34,6 +34,7 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { const FlutterBackingStoreConfig& config)>; using PresentCallback = std::function& layers)>; + using SurfaceTransformationCallback = std::function; //---------------------------------------------------------------------------- /// @brief Creates an external view embedder used by the generic embedder @@ -56,6 +57,17 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { /// ~EmbedderExternalViewEmbedder() override; + //---------------------------------------------------------------------------- + /// @brief Sets the surface transformation callback used by the external + /// view embedder to ask the platform for the per frame root + /// surface transformation. + /// + /// @param[in] surface_transformation_callback The surface transformation + /// callback + /// + void SetSurfaceTransformationCallback( + SurfaceTransformationCallback surface_transformation_callback); + private: // |ExternalViewEmbedder| void CancelFrame() override; @@ -108,12 +120,14 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { const CreateRenderTargetCallback create_render_target_callback_; const PresentCallback present_callback_; + SurfaceTransformationCallback surface_transformation_callback_; using Registry = std::unordered_map, RegistryKey::Hash, RegistryKey::Equal>; SkISize pending_frame_size_ = SkISize::Make(0, 0); + SkMatrix pending_surface_transformation_; std::map> pending_recorders_; std::map> pending_canvas_spies_; @@ -124,6 +138,8 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { void Reset(); + SkMatrix GetSurfaceTransformation() const; + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderExternalViewEmbedder); }; diff --git a/shell/platform/embedder/fixtures/compositor.png b/shell/platform/embedder/fixtures/compositor.png index a03c3cd4ff5a591da9354269979086dd04f2a340..038efaab9d8a880b028523437e52b400957d4993 100644 GIT binary patch literal 2614 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzEX7WqAsj$Z!;#X#z`*&| z)5S5QV$R#UhJGP|5^WEy9XePQw_KihA@Jty4V{Im*OC+1*B!WbvuCN=moIHiZ3+2b z-P_B1?EGts-sRq(^C(&jXeb`g5WjXAd;GMgkF)=Iq;A#O-u{oBf#FBItn4o!-SEGi zfsuj1h>3-P;RJ^O1A~IH15mUFo8Xy~`}Ax6r|!J@eQxzu$$8O{KI{w)wV#=o7#LDm zI2afv2r4izIJh+c-6F||Q}Elq7Q3%sKQBMsTvM@KPH$_!`eof^%nS!6h~u{%-ATn8 zRKKj`zkfgW&F6i~AAi2c^t&q3t^4Kdr>S4=PCZ(}!0-X&uoB`O_U{+aK>7UrduPt6 zwzHDeU2Pw8w_ogXd3yiiqhH>BEi1~|xqJ8K8GH-}>`!wO?Oo#SA5}?A@)`P}WI0nV zM)&mF7Z1`Z9_G~=TYf94sk5~FUQ<$K_{WHWp+F(d0ap%0&jaARdZvQ2Hpeu^N8WGQ z`st}#X8bECwT}X3O$NT>Bqr0F|Al_t$l5YvUu2xy_P6Kc+`C^gFdTST!ANWg0dXID zjMMx`Uh&J@uiw7@Pn3b-0e>ZFCIAbNQ7{?;qaiRF0>df<8t#7(J{YT7TdD9p3FI+P LS3j3^P60n@BOY(Ga z45^s&_Ku-nNT5X9Lu-c)R>dusCte7=d3!@=q3X5d1om|Y?%nKJs`lkeTT|NuvAV|( z9?X8e(*NAvyR$FXoMr+ViUtnc-ZV?DCUV;O^*@#WpUR5NzD z#}fkhLT6MCf9j!S_{{O)UpMV(SxJHMo`UWBX3A7s+3lY*r`pbHuSA_BLlH44Bw>$; z`8FxNQ@rX=!3nH-$1Zu@Q;ZH1iAfJ{>NWFnOd+ZAc3FD=Vqi+r_(V+VX4X4;c1>G% x(e2l7v;T83?1>~IKcQvnQSN96kP!khck(%uRBP9&&f^B@_H^}gS?83{1OQC46{r9J diff --git a/shell/platform/embedder/fixtures/compositor_root_surface_xformation.png b/shell/platform/embedder/fixtures/compositor_root_surface_xformation.png new file mode 100644 index 0000000000000000000000000000000000000000..8191fa524b45bac91fd0069f47c9bd660f101ad6 GIT binary patch literal 9485 zcmeHNYfuwc6y8LoF*O=md=M4ZpeR_fJk&}|Ak`2=<*AWSH3StTU=k$j5Uh`&jMA4* zRjdg}aga;|ic!WFK8VlyL~yk22x34HK@n{c7y;?s?2VP*9s6tf!=Ah6p6{IVop1k~ zd>a)J%3^Yu2!gP}=8Izxgf2snAt=KMoD3)?Tn7JW+L+L}NO|>yR`49BTq46|;fwgI zG%8-gYR$?-o=%kpybwg7%bCnlW{zak6{@Z8N7@Do+f<_<`) zjYANRm0{wqmgs2h^+zLIqFtP9aWkCuu#OgccuH1X@p!S&axT&~UR2l>-Zdrpg;&n) zS#`BBYq^syNavUorwon$-M9M7>Tex*9Qf#HPrwxwXA6Ukiu4GLK9qW(nm|ngr22mDpj1GqfKmaaf{&|$ zC(iquy?WnDBO@d4$Oq!zU=b^9O-)T3+X9Tch^?huuIOl9STY~3FwM(E){<`qOjF6p z^k*<+u2H#@NvmxP+tSuh)jHh1^2{1Rr!RF{G6hohLKjG znno*q*`HTH?n$qqs3K7$-A!8NWj0mo;|}AByR7!o_|lNpif2bAWIaFf$Is1p(dR^f z{YE_|TU=+(B!?xd7C_a;7Q)6UQt0&G@6AeaBe4`Xjh5OuIvExO?&@bA|IxUE1RJ+P zWaVrc#(_IqlP9VOR~0>-L>4F+%eF}3vz#E#;o*$Iu#&Uag|D}Fnc@yjd$_hG)T<@m zaKhTS@r+vEX4WHIRH6(N7_c7r{M0v$51SMDRRb3obu$4hL>p5F4ng`dgF$9T1< z%XjXIsqQb(1Ru)RdYY`g?R~S7S}Hw>Y&pX=UMN}jJc%$sGmkMDvbfCAkopuCwk4&$ z(f~^-1EnmLUx5|}Fu*yfPJ(2Y0s*Fmx+fb6Tk<{8MUuXzKqo2B`rWsU~(^&#Zs zM^DbD-c={Zmm5PC{1#i6SF~5#JIZ~JI=N~obBceG2Q}m)4j9v;OIVM zvn~8I0T))<>CCpgHD|&>Mr$UaiuE>~*x~GfD&9o@J<^f4QSn4KKp?+BDmzOC`&TA?#Z;FTaY?VHcbi&4E z1Eww>sX<7VMM@A)Ilguj*%`K6WMx*(9RdfW7)(#GN*oM*bu`-aJ|+uxd@uMqi1B%QlYbpRzEX7WqAsj$Z!;#X#z`*(5 z)5S5QV$R#U8*@V(CEPAnN~j1aJ2^24r5{lV*%;!qXId;1Yd}LRkLC&19Sky&(|r8C za8KLKajw(-y>FH2!R=x|L-BwE=eNB)czo-*|4&@CZbyk9-^a|r@Mrxzncqwd3dhwqhzpj3Gkhg8dKC|wZ3=9TOScx_S zY@JLlXYH%U+spO&fA0M~S$r|;w~ukZs$_JR<%?ZrWH`VL^v(s`-a+@{&)3s0EZ?{P z@2{GocMqT3%!vH^?d;hM!@sY-9zPmgy2lO2SrCdB25j~+v>M!iiJ$&@b zR%zX3=f7RLzN@y_ILe0|Xwq>KO|twW^y`LMl+XRO$1f)Ry880&`&ghcKOU2q-e)T7 zEj*tCbkX0Nbz5feF)+yVixX2czzY$EQ7{?;qaiRF0>do?ez4jzgvG1BciHQv1M-`v LtDnm{r-UW|q3J0h literal 1709 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y9Be?5)7S2I0V$SZC(jTLAgJL;>0n@BOYwAZ z45^s&_Ku-nXrP4K#bQ@s=?+y@*50{Wdly&q9g$*kE@0qeU{7A1$iU;E<g~d8TatZs&Lgh9AXEd<=~E zu>SYI2m8%mAKtfrXZY@ikM5=g@-_T#7q?$2=+2ep^6&4z`CQHKuD-Z+*T;Exs-$#JfsCkdi+jN!pg;hB zIIn-y_ixSjzh9r+d3hJy6oVy0n?dj_0vd$@? F2>_Jhf%*Ud diff --git a/shell/platform/embedder/fixtures/gradient.png b/shell/platform/embedder/fixtures/gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..27b69ecbb85c3378360309a32dc3bcc3bf7aa76f GIT binary patch literal 33551 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

_k(->wI;w#U|}e5e9_7;fK=ZHfYjGtyN`M6A-O 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

`^AIUc1YUp2!>W4@+PPWY2}6l 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 rainbow = [ + Color.fromARGB(255, 255, 0, 0), // red + Color.fromARGB(255, 255, 165, 0), // orange + Color.fromARGB(255, 255, 255, 0), // yellow + Color.fromARGB(255, 0, 255, 0), // green + Color.fromARGB(255, 0, 0, 255), // blue + Color.fromARGB(255, 75, 0, 130), // indigo + Color.fromARGB(255, 238,130,238), // violet + ]; + List stops = [ + (1.0 / 7.0), + (2.0 / 7.0), + (3.0 / 7.0), + (4.0 / 7.0), + (5.0 / 7.0), + (6.0 / 7.0), + (7.0 / 7.0), + ]; + paint.shader = Gradient.linear( + Offset(0.0, 0.0), + Offset(size.width, size.height), + rainbow, stops); + PictureRecorder baseRecorder = PictureRecorder(); + Canvas canvas = Canvas(baseRecorder); + canvas.drawRect(Rect.fromLTRB(0.0, 0.0, size.width, size.height), paint); + return baseRecorder.endRecording(); +} + +@pragma('vm:entry-point') +void render_gradient() { + window.onBeginFrame = (Duration duration) { + Size size = Size(800.0, 600.0); + + SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0.0, 0.0); + + builder.addPicture(Offset(0.0, 0.0), CreateGradientBox(size)); // gradient - flutter + + builder.pop(); + + window.render(builder.build()); + }; + window.scheduleFrame(); +} + +@pragma('vm:entry-point') +void render_gradient_on_non_root_backing_store() { + window.onBeginFrame = (Duration duration) { + Size size = Size(800.0, 600.0); + Color red = Color.fromARGB(127, 255, 0, 0); + + SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0.0, 0.0); + + // Even though this is occluded, add something so it is not elided. + builder.addPicture(Offset(10.0, 10.0), CreateColoredBox(red, size)); // red - flutter + + builder.addPlatformView(1, width: 100, height:200); // undefined - platform + + builder.addPicture(Offset(0.0, 0.0), CreateGradientBox(size)); // gradient - flutter + + builder.pop(); + + window.render(builder.build()); + }; + window.scheduleFrame(); +} diff --git a/shell/platform/embedder/fixtures/scene_without_custom_compositor.png b/shell/platform/embedder/fixtures/scene_without_custom_compositor.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3ee979b179dcbec2f54bb0df4fdadb61cf9bd4 GIT binary patch literal 2627 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefr0a_r;B4q#hkZy4gEp_CE6ZZJ9MxrZn-@1Lg3BY8#)VBuO%n2uRCz> zX3tW!FJIc4+7j}=y0@42*!kBMz018n=TWp6&`>;}A%5*L_V{T}A7}sbNZqQlz5O3M z1H+GaS=nDey5WC210w^25fcjo!wC)n1_lLX2cT#VHo-F|_vzRCPu+R*``qfSlJlY? zeb^ZqYCkhEF)*aCa4;}T5L94baByn?xCXC5XWyjx|51GsD4?=fB$~$o6q}}KmL4?>33D6TldS^PgB3#oqDu{f#CzlVI{;n z?B6e-f%5tL_s*PCZD%E`yV^eHZok;&^7Q`2N58!NT2_>^bNBAeGx!(|*q`Pm+PlQt zKdO?LO+*Ans$2ahe~=D}H(V_1o9~i83%e;IAaj1YiL&3PwXgTe~DWM4f@W3az literal 0 HcmV?d00001 diff --git a/shell/platform/embedder/fixtures/scene_without_custom_compositor_with_xform.png b/shell/platform/embedder/fixtures/scene_without_custom_compositor_with_xform.png new file mode 100644 index 0000000000000000000000000000000000000000..c80d8046cef0142cfb02e0a99418fc9ced41aecc GIT binary patch literal 9532 zcmeHNX;4#F6uyZ-V>B9C6hVb&P!uc)i&}{ZM1=rq*c1s>Ll8j%CP5O0U|oVTN;jRV zSQ3!pAejghgNz$4#I>#nj2FpSoz(tzaD>h=e~2#ch33F_x_xG z8y*@&qdHOn0JMb;ae`F#x!%4+&VjN=52w ztPHh_u(N57nP+o=R$1cW%8$S1@@l!^LRj-uUQw^0cUICXx175R8tNlAF=kwfTBH-7 z(X@E?-5D-#ymR=e?~}@YpKDUb9TYmoQv(v&ig+NJKuiLm0zw6Z3J4VtDj-xqsDMxb zp#nk$gbD~15Go*4K&XIF0igmy1%wI+6%Z;QR6wYJPywNWPpg7wws~!Cg9A}vVPW?~ zBeCyTq3fDkTU)nw`e^r=ww5B1xVvq6X)s!0>euj$rQeL`X5*9TO`!-K!gFWhR_A28 zp|h!`W2|}QnKhnFUTw2#7P6|Xt;(zWtnx!32yS3}?)4IxH%K|&9UL2p6_44>1Kes81zc2;J)2dm*l@0n#d|0yT zAyj>I5o(+`nN0ru!GdHb980Vuky3glC82`gUG@BvKWg{jU{ludjE>u^9nsEK=Zc91 z(&Fbc@B;bvbOS#&%Ld^b8%r4t%RhgU`*wG)F6QW*$D7)N+}aB?r;III&uMzIbuKVO z7KMCK9QfycC)AU0*dcwoto27Po7;&xi3qKt-7XOD1cx7Au=eWPI*GF)?TJAS}XZqn%NML9{#S-z(e*o!2=^y|A literal 0 HcmV?d00001 diff --git a/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/shell/platform/embedder/tests/embedder_a11y_unittests.cc index 739fab38c7097..f9255734f6323 100644 --- a/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -65,6 +65,7 @@ TEST_F(Embedder11yTest, A11yTreeIsConsistent) { }))); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("a11y_main"); auto engine = builder.LaunchEngine(); diff --git a/shell/platform/embedder/tests/embedder_assertions.h b/shell/platform/embedder/tests/embedder_assertions.h index bdc2a597368e4..a4918f56bca13 100644 --- a/shell/platform/embedder/tests/embedder_assertions.h +++ b/shell/platform/embedder/tests/embedder_assertions.h @@ -247,4 +247,18 @@ inline FlutterSize FlutterSizeMake(double width, double height) { return size; } +inline FlutterTransformation FlutterTransformationMake(const SkMatrix& matrix) { + FlutterTransformation transformation = {}; + transformation.scaleX = matrix[SkMatrix::kMScaleX]; + transformation.skewX = matrix[SkMatrix::kMSkewX]; + transformation.transX = matrix[SkMatrix::kMTransX]; + transformation.skewY = matrix[SkMatrix::kMSkewY]; + transformation.scaleY = matrix[SkMatrix::kMScaleY]; + transformation.transY = matrix[SkMatrix::kMTransY]; + transformation.pers0 = matrix[SkMatrix::kMPersp0]; + transformation.pers1 = matrix[SkMatrix::kMPersp1]; + transformation.pers2 = matrix[SkMatrix::kMPersp2]; + return transformation; +} + #endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_ASSERTIONS_H_ diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 0f38a78ea604f..c2ae44e516642 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -46,6 +46,12 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( return reinterpret_cast(context)->GLGetProcAddress( name); }; + opengl_renderer_config_.fbo_reset_after_present = true; + opengl_renderer_config_.surface_transformation = + [](void* context) -> FlutterTransformation { + return reinterpret_cast(context) + ->GetRootSurfaceTransformation(); + }; software_renderer_config_.struct_size = sizeof(FlutterSoftwareRendererConfig); software_renderer_config_.surface_present_callback = @@ -70,7 +76,6 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( AddCommandLineArgument("embedder_unittest"); if (preference == InitializationPreference::kInitialize) { - SetSoftwareRendererConfig(); SetAssetsPath(); SetSnapshots(); SetIsolateCreateCallbackHook(); @@ -85,15 +90,20 @@ FlutterProjectArgs& EmbedderConfigBuilder::GetProjectArgs() { return project_args_; } -void EmbedderConfigBuilder::SetSoftwareRendererConfig() { +void EmbedderConfigBuilder::SetSoftwareRendererConfig(SkISize surface_size) { renderer_config_.type = FlutterRendererType::kSoftware; renderer_config_.software = software_renderer_config_; + + // TODO(chinmaygarde): The compositor still uses a GL surface for operation. + // Once this is no longer the case, don't setup the GL surface when using the + // software renderer config. + context_.SetupOpenGLSurface(surface_size); } -void EmbedderConfigBuilder::SetOpenGLRendererConfig() { +void EmbedderConfigBuilder::SetOpenGLRendererConfig(SkISize surface_size) { renderer_config_.type = FlutterRendererType::kOpenGL; renderer_config_.open_gl = opengl_renderer_config_; - context_.SetupOpenGLSurface(); + context_.SetupOpenGLSurface(surface_size); } void EmbedderConfigBuilder::SetAssetsPath() { diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index 9dba0534369c2..561821705c462 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -43,9 +43,9 @@ class EmbedderConfigBuilder { FlutterProjectArgs& GetProjectArgs(); - void SetSoftwareRendererConfig(); + void SetSoftwareRendererConfig(SkISize surface_size = SkISize::Make(1, 1)); - void SetOpenGLRendererConfig(); + void SetOpenGLRendererConfig(SkISize surface_size); void SetAssetsPath(); diff --git a/shell/platform/embedder/tests/embedder_test_compositor.cc b/shell/platform/embedder/tests/embedder_test_compositor.cc index 7ff381ed763ee..a30dfcc9ce551 100644 --- a/shell/platform/embedder/tests/embedder_test_compositor.cc +++ b/shell/platform/embedder/tests/embedder_test_compositor.cc @@ -11,8 +11,10 @@ namespace flutter { namespace testing { -EmbedderTestCompositor::EmbedderTestCompositor(sk_sp context) - : context_(context) { +EmbedderTestCompositor::EmbedderTestCompositor(SkISize surface_size, + sk_sp context) + : surface_size_(surface_size), context_(context) { + FML_CHECK(!surface_size_.isEmpty()) << "Surface size must not be empty"; FML_CHECK(context_); } @@ -61,8 +63,7 @@ bool EmbedderTestCompositor::UpdateOffscrenComposition( size_t layers_count) { last_composition_ = nullptr; - auto surface_size = SkISize::Make(800, 600); - const auto image_info = SkImageInfo::MakeN32Premul(surface_size); + const auto image_info = SkImageInfo::MakeN32Premul(surface_size_); auto surface = type_ == RenderTargetType::kSoftwareBuffer ? SkSurface::MakeRaster(image_info) @@ -169,16 +170,16 @@ bool EmbedderTestCompositor::CreateFramebufferRenderSurface( const auto image_info = SkImageInfo::MakeN32Premul(config->size.width, config->size.height); - auto surface = - SkSurface::MakeRenderTarget(context_.get(), // context - SkBudgeted::kNo, // budgeted - image_info, // image info - 1, // sample count - kTopLeft_GrSurfaceOrigin, // surface origin - nullptr, // surface properties - false // mipmaps + auto surface = SkSurface::MakeRenderTarget( + context_.get(), // context + SkBudgeted::kNo, // budgeted + image_info, // image info + 1, // sample count + kBottomLeft_GrSurfaceOrigin, // surface origin + nullptr, // surface properties + false // mipmaps - ); + ); if (!surface) { FML_LOG(ERROR) << "Could not create render target for compositor layer."; @@ -219,16 +220,16 @@ bool EmbedderTestCompositor::CreateTextureRenderSurface( const auto image_info = SkImageInfo::MakeN32Premul(config->size.width, config->size.height); - auto surface = - SkSurface::MakeRenderTarget(context_.get(), // context - SkBudgeted::kNo, // budgeted - image_info, // image info - 1, // sample count - kTopLeft_GrSurfaceOrigin, // surface origin - nullptr, // surface properties - false // mipmaps + auto surface = SkSurface::MakeRenderTarget( + context_.get(), // context + SkBudgeted::kNo, // budgeted + image_info, // image info + 1, // sample count + kBottomLeft_GrSurfaceOrigin, // surface origin + nullptr, // surface properties + false // mipmaps - ); + ); if (!surface) { FML_LOG(ERROR) << "Could not create render target for compositor layer."; diff --git a/shell/platform/embedder/tests/embedder_test_compositor.h b/shell/platform/embedder/tests/embedder_test_compositor.h index 65c24cbbf9f71..ae6c768973a1c 100644 --- a/shell/platform/embedder/tests/embedder_test_compositor.h +++ b/shell/platform/embedder/tests/embedder_test_compositor.h @@ -22,7 +22,7 @@ class EmbedderTestCompositor { kSoftwareBuffer, }; - EmbedderTestCompositor(sk_sp context); + EmbedderTestCompositor(SkISize surface_size, sk_sp context); ~EmbedderTestCompositor(); @@ -59,6 +59,7 @@ class EmbedderTestCompositor { size_t GetBackingStoresCount() const; private: + const SkISize surface_size_; sk_sp context_; RenderTargetType type_ = RenderTargetType::kOpenGLFramebuffer; PlatformViewRendererCallback platform_view_renderer_callback_; diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index fd5867925b1e4..9403fef8126ec 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -5,6 +5,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_test_context.h" #include "flutter/runtime/dart_vm.h" +#include "flutter/shell/platform/embedder/tests/embedder_assertions.h" #include "third_party/skia/include/core/SkSurface.h" namespace flutter { @@ -59,6 +60,10 @@ const fml::Mapping* EmbedderTestContext::GetIsolateSnapshotInstructions() return isolate_snapshot_instructions_.get(); } +void EmbedderTestContext::SetRootSurfaceTransformation(SkMatrix matrix) { + root_surface_transformation_ = matrix; +} + void EmbedderTestContext::AddIsolateCreateCallback(fml::closure closure) { if (closure) { isolate_create_callbacks_.push_back(closure); @@ -126,10 +131,9 @@ EmbedderTestContext::GetUpdateSemanticsCustomActionCallbackHook() { }; } -void EmbedderTestContext::SetupOpenGLSurface() { - if (!gl_surface_) { - gl_surface_ = std::make_unique(); - } +void EmbedderTestContext::SetupOpenGLSurface(SkISize surface_size) { + FML_CHECK(!gl_surface_); + gl_surface_ = std::make_unique(surface_size); } bool EmbedderTestContext::GLMakeCurrent() { @@ -143,16 +147,11 @@ bool EmbedderTestContext::GLClearCurrent() { } bool EmbedderTestContext::GLPresent() { - gl_surface_present_count_++; FML_CHECK(gl_surface_) << "GL surface must be initialized."; + gl_surface_present_count_++; - if (next_scene_callback_) { - auto raster_snapshot = gl_surface_->GetRasterSurfaceSnapshot(); - FML_CHECK(raster_snapshot); - auto callback = next_scene_callback_; - next_scene_callback_ = nullptr; - callback(std::move(raster_snapshot)); - } + FireRootSurfacePresentCallbackIfPresent( + [&]() { return gl_surface_->GetRasterSurfaceSnapshot(); }); if (!gl_surface_->Present()) { return false; @@ -176,18 +175,21 @@ void* EmbedderTestContext::GLGetProcAddress(const char* name) { return gl_surface_->GetProcAddress(name); } +FlutterTransformation EmbedderTestContext::GetRootSurfaceTransformation() { + return FlutterTransformationMake(root_surface_transformation_); +} + void EmbedderTestContext::SetupCompositor() { - if (compositor_) { - return; - } - SetupOpenGLSurface(); - compositor_ = - std::make_unique(gl_surface_->GetGrContext()); + FML_CHECK(!compositor_) << "Already ssetup a compositor in this context."; + FML_CHECK(gl_surface_) + << "Setup the GL surface before setting up a compositor."; + compositor_ = std::make_unique( + gl_surface_->GetSurfaceSize(), gl_surface_->GetGrContext()); } EmbedderTestCompositor& EmbedderTestContext::GetCompositor() { FML_CHECK(compositor_) - << "Accessed the compositor on a context where one was not setup. Used " + << "Accessed the compositor on a context where one was not setup. Use " "the config builder to setup a context with a custom compositor."; return *compositor_; } @@ -203,8 +205,10 @@ void EmbedderTestContext::SetNextSceneCallback( bool EmbedderTestContext::SofwarePresent(sk_sp image) { software_surface_present_count_++; - software_surface_ = std::move(image); - return software_surface_ != nullptr; + + FireRootSurfacePresentCallbackIfPresent([image] { return image; }); + + return true; } size_t EmbedderTestContext::GetGLSurfacePresentCount() const { @@ -215,5 +219,15 @@ size_t EmbedderTestContext::GetSoftwareSurfacePresentCount() const { return software_surface_present_count_; } +void EmbedderTestContext::FireRootSurfacePresentCallbackIfPresent( + std::function(void)> image_callback) { + if (!next_scene_callback_) { + return; + } + auto callback = next_scene_callback_; + next_scene_callback_ = nullptr; + callback(image_callback()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 164134c22e0c3..9d37f0926ee03 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -42,6 +42,8 @@ class EmbedderTestContext { const fml::Mapping* GetIsolateSnapshotInstructions() const; + void SetRootSurfaceTransformation(SkMatrix matrix); + void AddIsolateCreateCallback(fml::closure closure); void AddNativeCallback(const char* name, Dart_NativeFunction function); @@ -54,8 +56,6 @@ class EmbedderTestContext { void SetPlatformMessageCallback( std::function callback); - void SetupCompositor(); - EmbedderTestCompositor& GetCompositor(); using NextSceneCallback = std::function image)>; @@ -80,9 +80,9 @@ class EmbedderTestContext { SemanticsActionCallback update_semantics_custom_action_callback_; std::function platform_message_callback_; std::unique_ptr gl_surface_; - sk_sp software_surface_; std::unique_ptr compositor_; NextSceneCallback next_scene_callback_; + SkMatrix root_surface_transformation_; size_t gl_surface_present_count_ = 0; size_t software_surface_present_count_ = 0; @@ -94,11 +94,13 @@ class EmbedderTestContext { static FlutterUpdateSemanticsCustomActionCallback GetUpdateSemanticsCustomActionCallbackHook(); + void SetupCompositor(); + void FireIsolateCreateCallbacks(); void SetNativeResolver(); - void SetupOpenGLSurface(); + void SetupOpenGLSurface(SkISize surface_size); bool GLMakeCurrent(); @@ -112,10 +114,15 @@ class EmbedderTestContext { void* GLGetProcAddress(const char* name); + FlutterTransformation GetRootSurfaceTransformation(); + void PlatformMessageCallback(const FlutterPlatformMessage* message); bool SofwarePresent(sk_sp image); + void FireRootSurfacePresentCallbackIfPresent( + std::function(void)> image_callback); + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderTestContext); }; diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 7e299fb6aa0a0..5304a959281d5 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -19,6 +19,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_assertions.h" #include "flutter/shell/platform/embedder/tests/embedder_config_builder.h" #include "flutter/shell/platform/embedder/tests/embedder_test.h" +#include "flutter/testing/assertions_skia.h" #include "flutter/testing/testing.h" #include "third_party/skia/include/core/SkSurface.h" #include "third_party/tonic/converter/dart_converter.h" @@ -41,6 +42,7 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithValidProjectArgs) { fml::AutoResetWaitableEvent latch; context.AddIsolateCreateCallback([&latch]() { latch.Signal(); }); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); // Wait for the root isolate to launch. @@ -50,6 +52,7 @@ TEST_F(EmbedderTest, CanLaunchAndShutdownWithValidProjectArgs) { TEST_F(EmbedderTest, CanLaunchAndShutdownMultipleTimes) { EmbedderConfigBuilder builder(GetEmbedderContext()); + builder.SetSoftwareRendererConfig(); for (size_t i = 0; i < 3; ++i) { auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -65,6 +68,7 @@ TEST_F(EmbedderTest, CanInvokeCustomEntrypoint) { }; context.AddNativeCallback("SayHiFromCustomEntrypoint", entrypoint); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("customEntrypoint"); auto engine = builder.LaunchEngine(); latch.Wait(); @@ -103,6 +107,7 @@ TEST_F(EmbedderTest, CanInvokeCustomEntrypointMacro) { })); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("customEntrypoint1"); auto engine = builder.LaunchEngine(); latch1.Wait(); @@ -192,6 +197,7 @@ TEST_F(EmbedderTest, CanSpecifyCustomTaskRunner) { EmbedderConfigBuilder builder(context); const auto task_runner_description = test_task_runner.GetFlutterTaskRunnerDescription(); + builder.SetSoftwareRendererConfig(); builder.SetPlatformTaskRunner(&task_runner_description); builder.SetDartEntrypoint("invokePlatformTaskRunner"); std::scoped_lock lock(engine_mutex); @@ -231,7 +237,7 @@ TEST(EmbedderTestNoFixture, CanGetCurrentTimeInNanoseconds) { TEST_F(EmbedderTest, CanCreateOpenGLRenderingEngine) { EmbedderConfigBuilder builder(GetEmbedderContext()); - builder.SetOpenGLRendererConfig(); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); } @@ -246,6 +252,7 @@ TEST_F(EmbedderTest, IsolateServiceIdSent) { thread.GetTaskRunner()->PostTask([&]() { EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("main"); builder.SetPlatformMessageCallback( [&](const FlutterPlatformMessage* message) { @@ -281,6 +288,7 @@ TEST_F(EmbedderTest, IsolateServiceIdSent) { TEST_F(EmbedderTest, CanCreateAndCollectCallbacks) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("platform_messages_response"); context.AddNativeCallback( "SignalNativeTest", @@ -318,6 +326,7 @@ TEST_F(EmbedderTest, PlatformMessagesCanReceiveResponse) { captures.thread_id = std::this_thread::get_id(); auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("platform_messages_response"); fml::AutoResetWaitableEvent ready; @@ -373,7 +382,7 @@ TEST_F(EmbedderTest, PlatformMessagesCanReceiveResponse) { TEST_F(EmbedderTest, PlatformMessagesCanBeSentWithoutResponseHandles) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); - + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("platform_messages_no_response"); const std::string message_data = "Hello but don't call me back."; @@ -418,7 +427,7 @@ TEST_F(EmbedderTest, PlatformMessagesCanBeSentWithoutResponseHandles) { TEST_F(EmbedderTest, NullPlatformMessagesCanBeSent) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); - + builder.SetSoftwareRendererConfig(); builder.SetDartEntrypoint("null_platform_messages"); fml::AutoResetWaitableEvent ready, message; @@ -460,7 +469,7 @@ TEST_F(EmbedderTest, NullPlatformMessagesCanBeSent) { TEST_F(EmbedderTest, InvalidPlatformMessages) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); - + builder.SetSoftwareRendererConfig(); auto engine = builder.LaunchEngine(); ASSERT_TRUE(engine.is_valid()); @@ -484,7 +493,7 @@ TEST_F(EmbedderTest, InvalidPlatformMessages) { TEST_F(EmbedderTest, VMShutsDownWhenNoEnginesInProcess) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); - + builder.SetSoftwareRendererConfig(); const auto launch_count = DartVM::GetVMLaunchCount(); { @@ -510,6 +519,7 @@ TEST_F(EmbedderTest, VMAndIsolateSnapshotSizesAreRedundantInAOTMode) { } auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); // The fixture sets this up correctly. Intentionally mess up the args. builder.GetProjectArgs().vm_snapshot_data_size = 0; @@ -530,6 +540,7 @@ TEST_F(EmbedderTest, MustPreventEngineLaunchWhenRequiredCompositorArgsAreAbsent) { auto& context = GetEmbedderContext(); EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); builder.SetCompositor(); builder.GetCompositor().create_backing_store_callback = nullptr; builder.GetCompositor().collect_backing_store_callback = nullptr; @@ -545,7 +556,10 @@ TEST_F(EmbedderTest, TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLFramebuffer) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLFramebuffer); @@ -607,10 +621,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLFramebuffer) { latch.CountDown(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -637,7 +647,10 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLFramebuffer) { TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLTexture) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); @@ -699,10 +712,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLTexture) { latch.CountDown(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -729,7 +738,10 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToOpenGLTexture) { TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToSoftwareBuffer) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kSoftwareBuffer); @@ -791,10 +803,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderToSoftwareBuffer) { latch.CountDown(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -944,7 +952,10 @@ static bool ImageMatchesFixture(const std::string& fixture_file_name, TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownScene) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); @@ -1083,10 +1094,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownScene) { return surface->makeImageSnapshot(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -1120,7 +1127,10 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownSceneWithSoftwareCompositor) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kSoftwareBuffer); @@ -1261,10 +1271,6 @@ TEST_F(EmbedderTest, return surface->makeImageSnapshot(); }); - EmbedderConfigBuilder builder(context); - builder.SetSoftwareRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -1297,6 +1303,12 @@ TEST_F(EmbedderTest, TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { auto& context = GetEmbedderContext(); + EmbedderConfigBuilder builder(context); + + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views"); + auto platform_task_runner = CreateNewThread("test_platform_thread"); static std::mutex engine_mutex; UniqueEngine engine; @@ -1311,8 +1323,6 @@ TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { ASSERT_EQ(FlutterEngineRunTask(engine.get(), &task), kSuccess); }); - context.SetupCompositor(); - context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); @@ -1376,12 +1386,8 @@ TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { const auto task_runner_description = test_task_runner.GetFlutterTaskRunnerDescription(); - EmbedderConfigBuilder builder(context); - builder.SetPlatformTaskRunner(&task_runner_description); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint("can_composite_platform_views"); + context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -1422,7 +1428,11 @@ TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithRootLayerOnly) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint( + "can_composite_platform_views_with_root_layer_only"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); @@ -1459,11 +1469,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithRootLayerOnly) { latch.CountDown(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint( - "can_composite_platform_views_with_root_layer_only"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -1493,7 +1498,11 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithRootLayerOnly) { TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithPlatformLayerOnBottom) { auto& context = GetEmbedderContext(); - context.SetupCompositor(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + builder.SetDartEntrypoint( + "can_composite_platform_views_with_platform_layer_on_bottom"); context.GetCompositor().SetRenderTargetType( EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); @@ -1572,11 +1581,6 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithPlatformLayerOnBottom) { return surface->makeImageSnapshot(); }); - EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(); - builder.SetCompositor(); - builder.SetDartEntrypoint( - "can_composite_platform_views_with_platform_layer_on_bottom"); context.AddNativeCallback( "SignalNativeTest", CREATE_NATIVE_ENTRY( @@ -1601,5 +1605,644 @@ TEST_F(EmbedderTest, CompositorMustBeAbleToRenderWithPlatformLayerOnBottom) { ASSERT_EQ(context.GetCompositor().GetBackingStoresCount(), 1u); } +//------------------------------------------------------------------------------ +/// Test the layer structure and pixels rendered when using a custom compositor +/// with a root surface transformation. +/// +TEST_F(EmbedderTest, + CompositorMustBeAbleToRenderKnownSceneWithRootSurfaceTransformation) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 800)); + builder.SetCompositor(); + builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); + + context.GetCompositor().SetRenderTargetType( + EmbedderTestCompositor::RenderTargetType::kOpenGLTexture); + + // This must match the transformation provided in the + // |CanRenderGradientWithoutCompositorWithXform| test to ensure that + // transforms are consistent respected. + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 800).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + fml::CountDownLatch latch(6); + + sk_sp scene_image; + context.SetNextSceneCallback([&](sk_sp scene) { + scene_image = std::move(scene); + latch.CountDown(); + }); + + context.GetCompositor().SetNextPresentCallback( + [&](const FlutterLayer** layers, size_t layers_count) { + ASSERT_EQ(layers_count, 5u); + + // Layer Root + { + FlutterBackingStore backing_store = *layers[0]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(600.0, 800.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[0], layer); + } + + // Layer 1 + { + FlutterPlatformView platform_view = {}; + platform_view.struct_size = sizeof(platform_view); + platform_view.identifier = 1; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypePlatformView; + layer.platform_view = &platform_view; + layer.size = FlutterSizeMake(150.0, 50.0); + layer.offset = FlutterPointMake(20.0, 730.0); + + ASSERT_EQ(*layers[1], layer); + } + + // Layer 2 + { + FlutterBackingStore backing_store = *layers[2]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(600.0, 800.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[2], layer); + } + + // Layer 3 + { + FlutterPlatformView platform_view = {}; + platform_view.struct_size = sizeof(platform_view); + platform_view.identifier = 2; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypePlatformView; + layer.platform_view = &platform_view; + layer.size = FlutterSizeMake(150.0, 50.0); + layer.offset = FlutterPointMake(40.0, 710.0); + + ASSERT_EQ(*layers[3], layer); + } + + // Layer 4 + { + FlutterBackingStore backing_store = *layers[4]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(600.0, 800.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[4], layer); + } + + latch.CountDown(); + }); + + context.GetCompositor().SetPlatformViewRendererCallback( + [&](const FlutterLayer& layer, GrContext* context) -> sk_sp { + auto surface = CreateRenderSurface(layer, context); + auto canvas = surface->getCanvas(); + FML_CHECK(canvas != nullptr); + + switch (layer.platform_view->identifier) { + case 1: { + SkPaint paint; + // See dart test for total order. + paint.setColor(SK_ColorGREEN); + paint.setAlpha(127); + const auto& rect = + SkRect::MakeWH(layer.size.width, layer.size.height); + canvas->drawRect(rect, paint); + latch.CountDown(); + } break; + case 2: { + SkPaint paint; + // See dart test for total order. + paint.setColor(SK_ColorMAGENTA); + paint.setAlpha(127); + const auto& rect = + SkRect::MakeWH(layer.size.width, layer.size.height); + canvas->drawRect(rect, paint); + latch.CountDown(); + } break; + default: + // Asked to render an unknown platform view. + FML_CHECK(false) + << "Test was asked to composite an unknown platform view."; + } + + return surface->makeImageSnapshot(); + }); + + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&latch](Dart_NativeArguments args) { latch.CountDown(); })); + + auto engine = builder.LaunchEngine(); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + // Flutter still thinks it is 800 x 600. Only the root surface is rotated. + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + ASSERT_TRUE(engine.is_valid()); + + latch.Wait(); + + ASSERT_TRUE(ImageMatchesFixture("compositor_root_surface_xformation.png", + scene_image)); +} + +TEST_F(EmbedderTest, CanRenderSceneWithoutCustomCompositor) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("can_render_scene_without_custom_compositor"); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("scene_without_custom_compositor.png", + renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderSceneWithoutCustomCompositorWithTransformation) { + auto& context = GetEmbedderContext(); + + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 800).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("can_render_scene_without_custom_compositor"); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 800)); + + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + + // Flutter still thinks it is 800 x 600. + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture( + "scene_without_custom_compositor_with_xform.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithoutCompositor) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("render_gradient"); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithoutCompositorWithXform) { + auto& context = GetEmbedderContext(); + + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 800).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + EmbedderConfigBuilder builder(context); + + const auto surface_size = SkISize::Make(600, 800); + + builder.SetDartEntrypoint("render_gradient"); + builder.SetOpenGLRendererConfig(surface_size); + + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + // Flutter still thinks it is 800 x 600. + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient_xform.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithCompositor) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("render_gradient"); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithCompositorWithXform) { + auto& context = GetEmbedderContext(); + + // This must match the transformation provided in the + // |CanRenderGradientWithoutCompositorWithXform| test to ensure that + // transforms are consistent respected. + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 800).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("render_gradient"); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 800)); + builder.SetCompositor(); + fml::CountDownLatch latch(1); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + // Flutter still thinks it is 800 x 600. + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient_xform.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayer) { + auto& context = GetEmbedderContext(); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("render_gradient_on_non_root_backing_store"); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetCompositor(); + fml::CountDownLatch latch(1); + + context.GetCompositor().SetNextPresentCallback( + [&](const FlutterLayer** layers, size_t layers_count) { + ASSERT_EQ(layers_count, 3u); + + // Layer Root + { + FlutterBackingStore backing_store = *layers[0]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(800.0, 600.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[0], layer); + } + + // Layer 1 + { + FlutterPlatformView platform_view = {}; + platform_view.struct_size = sizeof(platform_view); + platform_view.identifier = 1; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypePlatformView; + layer.platform_view = &platform_view; + layer.size = FlutterSizeMake(100.0, 200.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[1], layer); + } + + // Layer 2 + { + FlutterBackingStore backing_store = *layers[2]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(800.0, 600.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[2], layer); + } + }); + + context.GetCompositor().SetPlatformViewRendererCallback( + [&](const FlutterLayer& layer, GrContext* context) -> sk_sp { + auto surface = CreateRenderSurface(layer, context); + auto canvas = surface->getCanvas(); + FML_CHECK(canvas != nullptr); + + switch (layer.platform_view->identifier) { + case 1: { + FML_CHECK(layer.size.width == 100); + FML_CHECK(layer.size.height == 200); + // This is occluded anyway. We just want to make sure we see this. + } break; + default: + // Asked to render an unknown platform view. + FML_CHECK(false) + << "Test was asked to composite an unknown platform view."; + } + + return surface->makeImageSnapshot(); + }); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient.png", renderered_scene)); +} + +TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayerWithXform) { + auto& context = GetEmbedderContext(); + + // This must match the transformation provided in the + // |CanRenderGradientWithoutCompositorWithXform| test to ensure that + // transforms are consistent respected. + const auto root_surface_transformation = + SkMatrix().preTranslate(0, 800).preRotate(-90, 0, 0); + + context.SetRootSurfaceTransformation(root_surface_transformation); + + EmbedderConfigBuilder builder(context); + + builder.SetDartEntrypoint("render_gradient_on_non_root_backing_store"); + builder.SetOpenGLRendererConfig(SkISize::Make(600, 800)); + builder.SetCompositor(); + fml::CountDownLatch latch(1); + + context.GetCompositor().SetNextPresentCallback( + [&](const FlutterLayer** layers, size_t layers_count) { + ASSERT_EQ(layers_count, 3u); + + // Layer Root + { + FlutterBackingStore backing_store = *layers[0]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(600.0, 800.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[0], layer); + } + + // Layer 1 + { + FlutterPlatformView platform_view = {}; + platform_view.struct_size = sizeof(platform_view); + platform_view.identifier = 1; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypePlatformView; + layer.platform_view = &platform_view; + layer.size = FlutterSizeMake(200.0, 100.0); + layer.offset = FlutterPointMake(0.0, 700.0); + + ASSERT_EQ(*layers[1], layer); + } + + // Layer 2 + { + FlutterBackingStore backing_store = *layers[2]->backing_store; + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.did_update = true; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + + FlutterLayer layer = {}; + layer.struct_size = sizeof(layer); + layer.type = kFlutterLayerContentTypeBackingStore; + layer.backing_store = &backing_store; + layer.size = FlutterSizeMake(600.0, 800.0); + layer.offset = FlutterPointMake(0.0, 0.0); + + ASSERT_EQ(*layers[2], layer); + } + }); + + context.GetCompositor().SetPlatformViewRendererCallback( + [&](const FlutterLayer& layer, GrContext* context) -> sk_sp { + auto surface = CreateRenderSurface(layer, context); + auto canvas = surface->getCanvas(); + FML_CHECK(canvas != nullptr); + + switch (layer.platform_view->identifier) { + case 1: { + FML_CHECK(layer.size.width == 200); + FML_CHECK(layer.size.height == 100); + // This is occluded anyway. We just want to make sure we see this. + } break; + default: + // Asked to render an unknown platform view. + FML_CHECK(false) + << "Test was asked to composite an unknown platform view."; + } + + return surface->makeImageSnapshot(); + }); + + sk_sp renderered_scene; + context.SetNextSceneCallback([&](auto image) { + renderered_scene = std::move(image); + latch.CountDown(); + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + // Flutter still thinks it is 800 x 600. + event.width = 800; + event.height = 600; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + latch.Wait(); + + ASSERT_NE(renderered_scene, nullptr); + + ASSERT_TRUE(ImageMatchesFixture("gradient_xform.png", renderered_scene)); +} + } // namespace testing } // namespace flutter diff --git a/testing/BUILD.gn b/testing/BUILD.gn index b613afee0348c..037e7f6012fe6 100644 --- a/testing/BUILD.gn +++ b/testing/BUILD.gn @@ -47,6 +47,19 @@ source_set("dart") { ] } +source_set("skia") { + testonly = true + + sources = [ + "$flutter_root/testing/assertions_skia.h", + ] + + public_deps = [ + ":testing_lib", + "//third_party/skia", + ] +} + if (current_toolchain == host_toolchain) { source_set("opengl") { testonly = true @@ -59,8 +72,8 @@ if (current_toolchain == host_toolchain) { ] deps = [ + ":skia", "$flutter_root/fml", - "//third_party/skia", "//third_party/swiftshader_flutter:swiftshader", ] } diff --git a/testing/assertions_skia.h b/testing/assertions_skia.h new file mode 100644 index 0000000000000..2b501189a23ae --- /dev/null +++ b/testing/assertions_skia.h @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_TESTING_ASSERTIONS_SKIA_H_ +#define FLUTTER_TESTING_ASSERTIONS_SKIA_H_ + +#include + +#include "third_party/skia/include/core/SkMatrix.h" +#include "third_party/skia/include/core/SkMatrix44.h" +#include "third_party/skia/include/core/SkPoint3.h" +#include "third_party/skia/include/core/SkRRect.h" + +//------------------------------------------------------------------------------ +// Printing +//------------------------------------------------------------------------------ + +inline std::ostream& operator<<(std::ostream& os, const SkMatrix& m) { + os << std::endl; + os << "Scale X: " << m[SkMatrix::kMScaleX] << ", "; + os << "Skew X: " << m[SkMatrix::kMSkewX] << ", "; + os << "Trans X: " << m[SkMatrix::kMTransX] << std::endl; + os << "Skew Y: " << m[SkMatrix::kMSkewY] << ", "; + os << "Scale Y: " << m[SkMatrix::kMScaleY] << ", "; + os << "Trans Y: " << m[SkMatrix::kMTransY] << std::endl; + os << "Persp X: " << m[SkMatrix::kMPersp0] << ", "; + os << "Persp Y: " << m[SkMatrix::kMPersp1] << ", "; + os << "Persp Z: " << m[SkMatrix::kMPersp2]; + os << std::endl; + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkMatrix44& m) { + os << m.get(0, 0) << ", " << m.get(0, 1) << ", " << m.get(0, 2) << ", " + << m.get(0, 3) << std::endl; + os << m.get(1, 0) << ", " << m.get(1, 1) << ", " << m.get(1, 2) << ", " + << m.get(1, 3) << std::endl; + os << m.get(2, 0) << ", " << m.get(2, 1) << ", " << m.get(2, 2) << ", " + << m.get(2, 3) << std::endl; + os << m.get(3, 0) << ", " << m.get(3, 1) << ", " << m.get(3, 2) << ", " + << m.get(3, 3); + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkVector3& v) { + os << v.x() << ", " << v.y() << ", " << v.z(); + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkVector4& v) { + os << v.fData[0] << ", " << v.fData[1] << ", " << v.fData[2] << ", " + << v.fData[3]; + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkRect& r) { + os << "LTRB: " << r.fLeft << ", " << r.fTop << ", " << r.fRight << ", " + << r.fBottom; + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkRRect& r) { + os << "LTRB: " << r.rect().fLeft << ", " << r.rect().fTop << ", " + << r.rect().fRight << ", " << r.rect().fBottom; + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkPoint& r) { + os << "XY: " << r.fX << ", " << r.fY; + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const SkISize& size) { + os << size.width() << ", " << size.height(); + return os; +} + +#endif // FLUTTER_TESTING_ASSERTIONS_SKIA_H_ diff --git a/testing/test_gl_surface.cc b/testing/test_gl_surface.cc index e856ba452e429..90a9fb1f70f10 100644 --- a/testing/test_gl_surface.cc +++ b/testing/test_gl_surface.cc @@ -10,6 +10,7 @@ #include #include +#include "flutter/fml/build_config.h" #include "flutter/fml/logging.h" #include "third_party/skia/include/core/SkSurface.h" #include "third_party/skia/include/gpu/gl/GrGLAssembleInterface.h" @@ -79,10 +80,8 @@ static std::string GetEGLError() { return stream.str(); } -constexpr size_t kTestGLSurfaceWidth = 800; -constexpr size_t kTestGLSurfaceHeight = 600; - -TestGLSurface::TestGLSurface() { +TestGLSurface::TestGLSurface(SkISize surface_size) + : surface_size_(surface_size) { display_ = ::eglGetDisplay(EGL_DEFAULT_DISPLAY); FML_CHECK(display_ != EGL_NO_DISPLAY); @@ -113,24 +112,31 @@ TestGLSurface::TestGLSurface() { FML_CHECK(num_config == 1) << GetEGLError(); { - const EGLint surface_attributes[] = { - EGL_WIDTH, kTestGLSurfaceWidth, // - EGL_HEIGHT, kTestGLSurfaceHeight, // + const EGLint onscreen_surface_attributes[] = { + EGL_WIDTH, surface_size_.width(), // + EGL_HEIGHT, surface_size_.height(), // EGL_NONE, }; - onscreen_surface_ = - ::eglCreatePbufferSurface(display_, // display connection - config, // config - surface_attributes // surface attributes - ); + onscreen_surface_ = ::eglCreatePbufferSurface( + display_, // display connection + config, // config + onscreen_surface_attributes // surface attributes + ); FML_CHECK(onscreen_surface_ != EGL_NO_SURFACE) << GetEGLError(); + } - offscreen_surface_ = - ::eglCreatePbufferSurface(display_, // display connection - config, // config - surface_attributes // surface attributes - ); + { + const EGLint offscreen_surface_attributes[] = { + EGL_WIDTH, 1, // + EGL_HEIGHT, 1, // + EGL_NONE, + }; + offscreen_surface_ = ::eglCreatePbufferSurface( + display_, // display connection + config, // config + offscreen_surface_attributes // surface attributes + ); FML_CHECK(offscreen_surface_ != EGL_NO_SURFACE) << GetEGLError(); } @@ -178,8 +184,8 @@ TestGLSurface::~TestGLSurface() { FML_CHECK(result == EGL_TRUE); } -SkISize TestGLSurface::GetSize() const { - return SkISize::Make(kTestGLSurfaceWidth, kTestGLSurfaceHeight); +const SkISize& TestGLSurface::GetSurfaceSize() const { + return surface_size_; } bool TestGLSurface::MakeCurrent() { @@ -289,25 +295,29 @@ sk_sp TestGLSurface::CreateGrContext() { } sk_sp TestGLSurface::GetOnscreenSurface() { + FML_CHECK(::eglGetCurrentContext() != EGL_NO_CONTEXT); + GrGLFramebufferInfo framebuffer_info = {}; framebuffer_info.fFBOID = GetFramebuffer(); +#if OS_MACOSX framebuffer_info.fFormat = GR_GL_RGBA8; - - const auto size = GetSize(); +#else + framebuffer_info.fFormat = GR_GL_BGRA8; +#endif GrBackendRenderTarget backend_render_target( - size.width(), // width - size.height(), // height - 1, // sample count - 8, // stencil bits - framebuffer_info // framebuffer info + surface_size_.width(), // width + surface_size_.height(), // height + 1, // sample count + 8, // stencil bits + framebuffer_info // framebuffer info ); SkSurfaceProps surface_properties( SkSurfaceProps::InitType::kLegacyFontHost_InitType); auto surface = SkSurface::MakeFromBackendRenderTarget( - GetGrContext().get(), // context + GetGrContext().get(), // context backend_render_target, // backend render target kBottomLeft_GrSurfaceOrigin, // surface origin kN32_SkColorType, // color type diff --git a/testing/test_gl_surface.h b/testing/test_gl_surface.h index 6ba27194cf2a3..f75b0afee2c8c 100644 --- a/testing/test_gl_surface.h +++ b/testing/test_gl_surface.h @@ -15,11 +15,11 @@ namespace testing { class TestGLSurface { public: - TestGLSurface(); + TestGLSurface(SkISize surface_size); ~TestGLSurface(); - SkISize GetSize() const; + const SkISize& GetSurfaceSize() const; bool MakeCurrent(); @@ -50,6 +50,7 @@ class TestGLSurface { using EGLContext = void*; using EGLSurface = void*; + const SkISize surface_size_; EGLDisplay display_; EGLContext onscreen_context_; EGLContext offscreen_context_;