diff --git a/.cirrus.yml b/.cirrus.yml index 95bc2e9cc5384..c38fbb5b85941 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,11 +1,11 @@ gcp_credentials: ENCRYPTED[987a78af29b91ce8489594c9ab3fec21845bbe5ba68294b8f6def3cf0d380830f06687a89ea69c87344c5ade369700fe] web_shard_template: &WEB_SHARD_TEMPLATE - only_if: "changesInclude('.cirrus.yml', 'lib/web_ui/**', 'web_sdk/**') || $CIRRUS_PR == ''" + only_if: "changesInclude('.cirrus.yml', 'DEPS', 'lib/web_ui/**', 'web_sdk/**') || $CIRRUS_PR == ''" environment: - # As of October 2019, the Web shards needed more than 6G of RAM. - CPU: 2 - MEMORY: 8G + # As of March 2020, the Web shards needed 16G of RAM and 4 CPUs to run all framework tests with goldens without flaking. + CPU: 4 + MEMORY: 16G compile_host_script: | cd $ENGINE_PATH/src ./flutter/tools/gn --unoptimized --full-dart-sdk @@ -60,10 +60,23 @@ task: cd $ENGINE_PATH/src/out/host_release/ ./txt_benchmarks --benchmark_format=json > txt_benchmarks.json ./fml_benchmarks --benchmark_format=json > fml_benchmarks.json + ./shell_benchmarks --benchmark_format=json > shell_benchmarks.json cd $ENGINE_PATH/src/flutter/testing/benchmark pub get dart bin/parse_and_send.dart ../../../out/host_release/txt_benchmarks.json dart bin/parse_and_send.dart ../../../out/host_release/fml_benchmarks.json + dart bin/parse_and_send.dart ../../../out/host_release/shell_benchmarks.json + - name: build_and_test_linux_release + compile_host_script: | + cd $ENGINE_PATH/src + ./flutter/tools/gn --runtime-mode=release + ninja -C out/host_release + test_host_script: | + cd $ENGINE_PATH/src + ./flutter/testing/run_tests.sh host_release + + # The following test depends on Flutter framework repo. It may fail if the + # framework repo is currently broken. - name: build_and_test_linux_unopt_debug compile_host_script: | cd $ENGINE_PATH/src @@ -126,6 +139,16 @@ task: - name: web_tests-7_last-linux # last Web shard must end with _last << : *WEB_SHARD_TEMPLATE + - name: web_engine_analysis + compile_host_script: | + cd $ENGINE_PATH/src + ./flutter/tools/gn --unoptimized --full-dart-sdk + ninja -C out/host_debug_unopt + script: + - cd $ENGINE_PATH/src/flutter/lib/web_ui + - $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get + - $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dartanalyzer --fatal-warnings --fatal-hints dev/ lib/ test/ tool/ + - name: web_engine_integration_test_linux compile_host_script: | cd $ENGINE_PATH/src @@ -146,6 +169,9 @@ task: - $FRAMEWORK_PATH/flutter/bin/flutter config --local-engine=host_debug_unopt --no-analytics --enable-web - $FRAMEWORK_PATH/flutter/bin/flutter pub get --local-engine=host_debug_unopt - $FRAMEWORK_PATH/flutter/bin/flutter drive -v --target=test_driver/text_editing_e2e.dart -d web-server --release --browser-name=chrome --local-engine=host_debug_unopt + - $FRAMEWORK_PATH/flutter/bin/flutter drive -v --target=test_driver/platform_messages_e2e.dart -d web-server --release --browser-name=chrome --local-engine=host_debug_unopt + - $FRAMEWORK_PATH/flutter/bin/flutter drive -v --target=test_driver/treeshaking_e2e.dart -d web-server --profile --browser-name=chrome --local-engine=host_debug_unopt + - $FRAMEWORK_PATH/flutter/bin/flutter drive -v --target=test_driver/image_loading_e2e.dart -d web-server --release --browser-name=chrome --local-engine=host_debug_unopt - name: build_and_test_web_linux_firefox compile_host_script: | @@ -186,40 +212,3 @@ task: cd $ENGINE_PATH/src ./flutter/tools/fuchsia/build_fuchsia_artifacts.py --engine-version HEAD --runtime-mode debug --no-lto --archs x64 cd $ENGINE_PATH/src/flutter - -# WINDOWS -task: - gce_instance: - image_project: flutter-cirrus - image_name: flutter-engine-windows-server-2016 - zone: us-central1-a - platform: windows - cpu: 32 - memory: 32Gb - disk: 50 - env: - # Cirrus is somehow not picking up the environment variables set in the VM image. - PATH: "c:/depot_tools;c:/MinGit/cmd;$PATH" - DEPOT_TOOLS_WIN_TOOLCHAIN: 0 - GYP_MSVS_OVERRIDE_PATH: "c:/Program Files (x86)/Microsoft Visual Studio/2017/Community" - ENGINE_PATH: "c:/flutter/engine" - setup_script: | - REM robocopy can return 1 for successful copy; suppress its error code. - REM move somehow doesn't work as it complains that the file is being used by another process. - robocopy %CIRRUS_WORKING_DIR% %ENGINE_PATH%/src/flutter /MIR| (cmd /s /c exit /b 0) - cd %ENGINE_PATH%/src - gclient sync - matrix: - - name: build_and_test_windows_unopt_debug - compile_host_script: | - cd %ENGINE_PATH%/src - python flutter/tools/gn --runtime-mode debug --unoptimized - ninja -C out/host_debug_unopt - test_host_script: | - cd %ENGINE_PATH%/src - python flutter/testing/run_tests.py --type=engine - - name: build_windows_opt_debug - compile_host_script: | - cd %ENGINE_PATH%/src - python flutter/tools/gn --runtime-mode debug - ninja -C out/host_debug diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index 7bdabfbc1e3a7..638c686bd5cdc 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -20,7 +20,6 @@ reviewers: - franciscojma86 - cbracken - flar - - stuartmorgan # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) diff --git a/BUILD.gn b/BUILD.gn index 25006866ca896..fea2a2b5bebf1 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -19,7 +19,7 @@ group("flutter") { public_deps = [] - if (!(is_fuchsia && using_fuchsia_sdk)) { + if (!is_fuchsia) { public_deps += [ "//flutter/lib/snapshot:generate_snapshot_bin", "//flutter/lib/snapshot:kernel_platform_files", @@ -40,14 +40,14 @@ group("flutter") { public_deps += [ "//flutter/tools/const_finder" ] } - if (is_fuchsia && using_fuchsia_sdk) { + if (is_fuchsia) { public_deps += [ "//flutter/shell/platform/fuchsia", "//flutter/shell/testing($host_toolchain)", ] } - if (!is_fuchsia && !is_fuchsia_host) { + if (!is_fuchsia) { if (current_toolchain == host_toolchain) { public_deps += [ "//flutter/flutter_frontend_server:frontend_server", @@ -75,6 +75,8 @@ group("flutter") { "//flutter/lib/ui:ui_unittests", "//flutter/runtime:runtime_unittests", "//flutter/shell/common:shell_unittests", + "//flutter/shell/platform/common/cpp:common_cpp_core_unittests", + "//flutter/shell/platform/common/cpp:common_cpp_unittests", "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper_unittests", "//flutter/shell/platform/embedder:embedder_unittests", "//flutter/shell/platform/glfw/client_wrapper:client_wrapper_glfw_unittests", diff --git a/DEPS b/DEPS index 92e3f4f7dfb4e..1af4d736d7562 100644 --- a/DEPS +++ b/DEPS @@ -26,7 +26,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': '320c32bfa3062b0c67270667df5e4df1856f176e', + 'skia_revision': 'ad653d8378d7a17502956c4addebb68eb3129961', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -34,12 +34,12 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '7c5059f528814e14dc9ff29ee1629b922daf77a4', + 'dart_revision': '3e43a3dcadf96c0f1e30b12e0a1805df5a336c3c', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py - 'dart_args_tag': '1.5.3', - 'dart_async_tag': '2.0.8', + 'dart_args_tag': '1.6.0', + 'dart_async_tag': '2.4.1', 'dart_bazel_worker_tag': 'v0.1.22', 'dart_boolean_selector_tag': '1.0.4', 'dart_boringssl_gen_rev': 'b9e27cff1ff0803e97ab1f88764a83be4aa94a6d', @@ -60,9 +60,9 @@ vars = { 'dart_http_retry_tag': '0.1.1', 'dart_http_tag': '0.12.0+2', 'dart_http_throttle_tag': '1.0.2', - 'dart_intl_tag': '0.15.7', + 'dart_intl_tag': '0.16.1', 'dart_json_rpc_2_tag': '2.0.9', - 'dart_linter_tag': '0.1.112', + 'dart_linter_tag': '0.1.115', 'dart_logging_tag': '0.11.3+2', 'dart_markdown_tag': '2.1.1', 'dart_matcher_tag': '0.12.5', @@ -71,14 +71,13 @@ vars = { 'dart_mustache_tag': '5e81b12215566dbe2473b2afd01a8a8aedd56ad9', 'dart_oauth2_tag': '1.2.1', 'dart_observatory_pub_packages_rev': '0894122173b0f98eb08863a7712e78407d4477bc', - 'dart_package_config_tag': '87a8b5184020ebcc13b34ee95dde58f851b68ca3', - 'dart_package_resolver_tag': '1.0.10', + 'dart_package_config_tag': 'v1.9.2', 'dart_path_tag': '1.6.2', - 'dart_pedantic_tag': 'v1.8.0', + 'dart_pedantic_tag': 'v1.9.0', 'dart_pool_tag': '1.3.6', 'dart_protobuf_rev': '3746c8fd3f2b0147623a8e3db89c3ff4330de760', - 'dart_pub_rev': '429a06039d185149f387a65e3503b0693ce6d24e', - 'dart_pub_semver_tag': '1.4.2', + 'dart_pub_rev': '3606265962da4248d34d352aa3d170aae4496a90', + 'dart_pub_semver_tag': 'v1.4.4', 'dart_quiver-dart_tag': '2.0.0+1', 'dart_resource_rev': 'f8e37558a1c4f54550aa463b88a6a831e3e33cd6', 'dart_root_certificates_rev': '16ef64be64c7dfdff2b9f4b910726e635ccc519e', @@ -88,7 +87,7 @@ vars = { 'dart_shelf_web_socket_tag': '0.2.2+3', 'dart_source_map_stack_trace_tag': '2.0.0', 'dart_source_maps_tag': '8af7cc1a1c3a193c1fba5993ce22a546a319c40e', - 'dart_source_span_tag': '1.5.5', + 'dart_source_span_tag': '1.7.0', 'dart_stack_trace_tag': '1.9.3', 'dart_stagehand_tag': 'v3.3.7', 'dart_stream_channel_tag': '2.0.0', @@ -100,7 +99,7 @@ vars = { 'dart_typed_data_tag': '1.1.6', 'dart_usage_tag': '3.4.0', 'dart_watcher_rev': '0.9.7+14', - 'dart_web_socket_channel_tag': '1.0.9', + 'dart_web_socket_channel_tag': '1.0.15', 'dart_yaml_tag': '2.2.0', 'ocmock_tag': 'v3.4.3', @@ -114,6 +113,9 @@ vars = { # Checkout Windows dependencies only if we are building on Windows. 'download_windows_deps' : 'host_os == "win"', + # Checkout Linux dependencies only when building on Linux. + 'download_linux_deps': 'host_os == "linux"', + # An LLVM backend needs LLVM binaries and headers. To avoid build time # increases we can use prebuilts. We don't want to download this on every # CQ/CI bot nor do we want the average Dart developer to incur that cost. @@ -137,7 +139,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'd14f3c708fb132381f7053b4ee9e628be915ed96', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + '036715c76da60220b39312ea066cd65d32c2157d', # Fuchsia compatibility # @@ -177,6 +179,9 @@ deps = { 'src/third_party/icu': Var('chromium_git') + '/chromium/deps/icu.git' + '@' + '5005010d694e16571b8dfbf07d70817841f80a69', + 'src/third_party/khronos': + Var('chromium_git') + '/chromium/src/third_party/khronos.git' + '@' + '7122230e90547962e0f0c627f62eeed3c701f275', + 'src/third_party/boringssl': Var('github_git') + '/dart-lang/boringssl_gen.git' + '@' + Var('dart_boringssl_gen_rev'), @@ -228,7 +233,7 @@ deps = { Var('dart_git') + '/dart2js_info.git' + '@' + Var('dart_dart2js_info_tag'), 'src/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@v0.30.2', + Var('dart_git') + '/dartdoc.git@v0.30.3', 'src/third_party/dart/third_party/pkg/ffi': Var('dart_git') + '/ffi.git' + '@' + Var('dart_ffi_tag'), @@ -377,11 +382,8 @@ deps = { 'src/third_party/dart/third_party/pkg_tested/package_config': Var('dart_git') + '/package_config.git' + '@' + Var('dart_package_config_tag'), - 'src/third_party/dart/third_party/pkg_tested/package_resolver': - Var('dart_git') + '/package_resolver.git' + '@' + Var('dart_package_resolver_tag'), - 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.8.0-dev.0.0', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.8.0-dev.18.0', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, # WARNING: end of dart dependencies list that is cleaned up automatically - see create_updated_flutter_deps.py. @@ -432,7 +434,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/ndk/${{platform}}', - 'version': 'version:r19b' + 'version': 'version:r21.0.6113669' } ], 'condition': 'download_android_deps', @@ -521,8 +523,8 @@ deps = { 'src/buildtools/{host_os}-x64/clang': { 'packages': [ { - 'package': 'fuchsia/clang/${{platform}}', - 'version': 'git_revision:de39621f0f03f20633bdfa50bde97a3908bf6e98' + 'package': 'fuchsia/third_party/clang/${{platform}}', + 'version': 'git_revision:7e9747b50bcb1be28d4a3236571e8050835497a6' } ], 'condition': 'host_os == "mac" or host_os == "linux"', @@ -536,7 +538,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'eGWV8Up-Giv-15K1FBRnkFQompVx2GpVBNomG09my_wC' + 'version': '8JtFK64mmIC2zTEj9ICMrcQBITqKDZVQluLVKczro9kC' } ], 'condition': 'host_os == "mac"', @@ -556,7 +558,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': '6ds94xRmD_5jjJ9jn1pngltBcWUyduH4JF-PxklsD50C' + 'version': 'LnaL23_DpQsbnbs-byJi-UoGe1XerKCfLjb4_XkxMRoC' } ], 'condition': 'host_os == "linux"', @@ -626,4 +628,13 @@ hooks = [ 'src/third_party/dart/third_party/7zip.tar.gz.sha1', ], }, + { + 'name': 'linux_sysroot', + 'pattern': '.', + 'condition': 'download_linux_deps', + 'action': [ + 'python', + 'src/build/linux/sysroot_scripts/install-sysroot.py', + '--arch=x64'], + }, ] diff --git a/README.md b/README.md index 0c4190c9aba64..eb16aa51bb3e0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ Flutter Engine ============== + [![Build Status - Cirrus][]][Build status] Flutter is Google's mobile app SDK for crafting high-quality native interfaces diff --git a/analysis_options.yaml b/analysis_options.yaml index 64c4409491bda..c125e1d52f749 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,9 +10,13 @@ # private fields, especially on the Window object): analyzer: - # this test pretends to be part of dart:ui and results in lots of false - # positives. - exclude: [ testing/dart/window_hooks_integration_test.dart ] + exclude: [ + # this test pretends to be part of dart:ui and results in lots of false + # positives. + testing/dart/window_hooks_integration_test.dart, + # Fixture depends on dart:ui and raises false positives. + flutter_frontend_server/test/fixtures/lib/main.dart + ] strong-mode: implicit-casts: false implicit-dynamic: false diff --git a/ci/check_gn_format.py b/ci/check_gn_format.py index d7611a4f91415..2257a58984dc6 100755 --- a/ci/check_gn_format.py +++ b/ci/check_gn_format.py @@ -41,8 +41,8 @@ def main(): for gn_file in GetGNFiles(args.root_directory): if subprocess.call(gn_command + [ gn_file ]) != 0: print "ERROR: '%s' is incorrectly formatted." % os.path.relpath(gn_file, args.root_directory) - print "Format the same with 'gn format' using the 'gn' binary in //buildtools." - print "Or, run ./ci/check_gn_format.py with '--dry-run false'" + print "Format the same with 'gn format' using the 'gn' binary in third_party/gn/gn." + print "Or, run ./ci/check_gn_format.py without '--dry-run'" return 1 return 0 diff --git a/ci/format.sh b/ci/format.sh index 254d38fbc43a1..979cf684f2b0a 100755 --- a/ci/format.sh +++ b/ci/format.sh @@ -42,7 +42,7 @@ fi; BASE_SHA="$(git fetch $UPSTREAM master > /dev/null 2>&1 && \ (git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD))" -CLANG_FILES_TO_CHECK="$(git diff $DIFF_OPTS $BASE_SHA -- $CLANG_FILETYPES)" +CLANG_FILES_TO_CHECK="$(git ls-files $CLANG_FILETYPES)" FAILED_CHECKS=0 for f in $CLANG_FILES_TO_CHECK; do set +e diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index ef7478a6914a0..efcc72ebf43e9 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -49,6 +49,7 @@ FILE: ../../../flutter/flow/layers/color_filter_layer_unittests.cc FILE: ../../../flutter/flow/layers/container_layer.cc FILE: ../../../flutter/flow/layers/container_layer.h FILE: ../../../flutter/flow/layers/container_layer_unittests.cc +FILE: ../../../flutter/flow/layers/fuchsia_layer_unittests.cc FILE: ../../../flutter/flow/layers/image_filter_layer.cc FILE: ../../../flutter/flow/layers/image_filter_layer.h FILE: ../../../flutter/flow/layers/image_filter_layer_unittests.cc @@ -107,6 +108,9 @@ FILE: ../../../flutter/flow/view_holder.cc FILE: ../../../flutter/flow/view_holder.h FILE: ../../../flutter/flutter_frontend_server/bin/starter.dart FILE: ../../../flutter/flutter_frontend_server/lib/server.dart +FILE: ../../../flutter/fml/ascii_trie.cc +FILE: ../../../flutter/fml/ascii_trie.h +FILE: ../../../flutter/fml/ascii_trie_unittests.cc FILE: ../../../flutter/fml/backtrace.cc FILE: ../../../flutter/fml/backtrace.h FILE: ../../../flutter/fml/backtrace_stub.cc @@ -130,9 +134,6 @@ FILE: ../../../flutter/fml/eintr_wrapper.h FILE: ../../../flutter/fml/file.cc FILE: ../../../flutter/fml/file.h FILE: ../../../flutter/fml/file_unittest.cc -FILE: ../../../flutter/fml/gpu_thread_merger.cc -FILE: ../../../flutter/fml/gpu_thread_merger.h -FILE: ../../../flutter/fml/gpu_thread_merger_unittests.cc FILE: ../../../flutter/fml/hash_combine.h FILE: ../../../flutter/fml/hash_combine_unittests.cc FILE: ../../../flutter/fml/icu_util.cc @@ -186,6 +187,7 @@ FILE: ../../../flutter/fml/platform/android/scoped_java_ref.cc FILE: ../../../flutter/fml/platform/android/scoped_java_ref.h FILE: ../../../flutter/fml/platform/darwin/cf_utils.cc FILE: ../../../flutter/fml/platform/darwin/cf_utils.h +FILE: ../../../flutter/fml/platform/darwin/cf_utils_unittests.mm FILE: ../../../flutter/fml/platform/darwin/message_loop_darwin.h FILE: ../../../flutter/fml/platform/darwin/message_loop_darwin.mm FILE: ../../../flutter/fml/platform/darwin/paths_darwin.mm @@ -224,6 +226,9 @@ FILE: ../../../flutter/fml/platform/win/paths_win.cc FILE: ../../../flutter/fml/platform/win/posix_wrappers_win.cc FILE: ../../../flutter/fml/platform/win/wstring_conversion.h FILE: ../../../flutter/fml/posix_wrappers.h +FILE: ../../../flutter/fml/raster_thread_merger.cc +FILE: ../../../flutter/fml/raster_thread_merger.h +FILE: ../../../flutter/fml/raster_thread_merger_unittests.cc FILE: ../../../flutter/fml/size.h FILE: ../../../flutter/fml/status.h FILE: ../../../flutter/fml/synchronization/atomic_object.h @@ -266,6 +271,7 @@ FILE: ../../../flutter/lib/io/dart_io.cc FILE: ../../../flutter/lib/io/dart_io.h FILE: ../../../flutter/lib/snapshot/libraries.json FILE: ../../../flutter/lib/snapshot/snapshot.h +FILE: ../../../flutter/lib/ui/annotations.dart FILE: ../../../flutter/lib/ui/channel_buffers.dart FILE: ../../../flutter/lib/ui/compositing.dart FILE: ../../../flutter/lib/ui/compositing/scene.cc @@ -442,6 +448,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -490,7 +497,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/web_experiments.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart +FILE: ../../../flutter/lib/web_ui/lib/src/ui/annotations.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/channel_buffers.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/compositing.dart @@ -583,6 +592,8 @@ FILE: ../../../flutter/shell/common/shell_io_manager.cc FILE: ../../../flutter/shell/common/shell_io_manager.h FILE: ../../../flutter/shell/common/shell_test.cc FILE: ../../../flutter/shell/common/shell_test.h +FILE: ../../../flutter/shell/common/shell_test_external_view_embedder.cc +FILE: ../../../flutter/shell/common/shell_test_external_view_embedder.h FILE: ../../../flutter/shell/common/shell_test_platform_view.cc FILE: ../../../flutter/shell/common/shell_test_platform_view.h FILE: ../../../flutter/shell/common/shell_test_platform_view_gl.cc @@ -769,22 +780,19 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/encodable_value.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/engine_method_result.h -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_type.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/message_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_call.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registry.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_message_codec.h FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/json_message_codec.cc -FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/json_method_codec.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_call_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc +FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_codec.cc @@ -793,6 +801,15 @@ FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_message FILE: ../../../flutter/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/incoming_message_dispatcher.cc FILE: ../../../flutter/shell/platform/common/cpp/incoming_message_dispatcher.h +FILE: ../../../flutter/shell/platform/common/cpp/json_message_codec.cc +FILE: ../../../flutter/shell/platform/common/cpp/json_message_codec.h +FILE: ../../../flutter/shell/platform/common/cpp/json_message_codec_unittests.cc +FILE: ../../../flutter/shell/platform/common/cpp/json_method_codec.cc +FILE: ../../../flutter/shell/platform/common/cpp/json_method_codec.h +FILE: ../../../flutter/shell/platform/common/cpp/json_method_codec_unittests.cc +FILE: ../../../flutter/shell/platform/common/cpp/path_utils.cc +FILE: ../../../flutter/shell/platform/common/cpp/path_utils.h +FILE: ../../../flutter/shell/platform/common/cpp/path_utils_unittests.cc FILE: ../../../flutter/shell/platform/common/cpp/public/flutter_export.h FILE: ../../../flutter/shell/platform/common/cpp/public/flutter_messenger.h FILE: ../../../flutter/shell/platform/common/cpp/public/flutter_plugin_registrar.h @@ -855,14 +872,19 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginA FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm -FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h @@ -882,6 +904,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/ios_context_software.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_context_software.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_gl.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_gl.mm +FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_metal.h +FILE: ../../../flutter/shell/platform/darwin/ios/ios_external_texture_metal.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_render_target_gl.h FILE: ../../../flutter/shell/platform/darwin/ios/ios_render_target_gl.mm FILE: ../../../flutter/shell/platform/darwin/ios/ios_surface.h @@ -1132,11 +1156,18 @@ FILE: ../../../flutter/shell/platform/glfw/platform_handler.h FILE: ../../../flutter/shell/platform/glfw/public/flutter_glfw.h FILE: ../../../flutter/shell/platform/glfw/text_input_plugin.cc FILE: ../../../flutter/shell/platform/glfw/text_input_plugin.h +FILE: ../../../flutter/shell/platform/linux/fl_dart_project.cc +FILE: ../../../flutter/shell/platform/linux/fl_view.cc +FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_dart_project.h +FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_view.h +FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/flutter_linux.h FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.cc FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.h +FILE: ../../../flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc FILE: ../../../flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc +FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plugin_registrar_windows.h diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index 336439ae8a093..5e24346dc2215 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 8f652d8bf0e8a2e1e3edcf50cf29c0e8 +Signature: adc98c90de1d424da0f0546ff7689965 UNUSED LICENSES: @@ -461,8 +461,10 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera2/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castauth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castconfig/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castremotecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsetup/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsysteminfo/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castwindow/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.cobalt/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json @@ -534,7 +536,9 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channelcontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.url/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.web/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.common/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.policy/meta.json @@ -1267,8 +1271,6 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/vmo/vmo_writer.dar FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/testing.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/meta.json FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_logger/meta.json -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/internal/_streaming_intent_handler_impl.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/streaming_intent_handler.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/meta.json FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/agent_interceptor.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/test_harness_fixtures.dart @@ -1370,10 +1372,12 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castauth/cast_auth.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castauth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castconfig/cast_config.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castconfig/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castremotecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsetup/cast_setup.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsetup/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsysteminfo/cast_system_info.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsysteminfo/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castwindow/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.cobalt/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json @@ -1532,7 +1536,9 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channel/channel.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channelcontrol/channelcontrol.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channelcontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.url/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.web/constants.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.web/context.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.web/cookie.fidl @@ -2193,8 +2199,10 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera2/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castauth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castconfig/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castremotecontrol/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsetup/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsysteminfo/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castwindow/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.cobalt/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json @@ -2266,7 +2274,9 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update.channelcontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.url/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.web/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.common/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.policy/meta.json @@ -2587,7 +2597,6 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_logger/lib/src/internal/_stdout_lo FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_logger/lib/src/logger/logger.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/agent.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/codecs.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/entity.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/lifecycle.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/logger.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/module.dart @@ -2595,22 +2604,16 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/service_connection.dar FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/agent/agent.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/agent/internal/_agent_context.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/agent/internal/_agent_impl.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/entity/entity.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/entity/entity_exceptions.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/entity/internal/_entity_impl.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/internal/_component_context.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/lifecycle/internal/_lifecycle_impl.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/lifecycle/lifecycle.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/embedded_module.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/intent.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/intent_handler.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/internal/_fidl_transformers.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/internal/_intent_handler_impl.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/internal/_module_context.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/internal/_module_impl.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/module.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/module_state_exception.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/noop_intent_handler.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/service_connection/agent_service_connection.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/child_view.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/child_view_connection.dart @@ -2927,10 +2930,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/agent/agent_context.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/agent/agent_controller/agent_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/component/component_context.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/config/config.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/entity/entity.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/entity/entity_provider.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/entity/entity_reference_factory.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/entity/entity_resolver.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/intent/intent.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/lifecycle/lifecycle.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/module/link_path.fidl @@ -3246,6 +3245,8 @@ FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/noop_view_p FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular/lib/src/module/view_provider.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_interceptor.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_modular_testing/lib/src/module_with_view_provider_impl.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/device.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/launch.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/power.dart FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/update.dart FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/audio_mode.fidl @@ -3253,7 +3254,9 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.le/connection_options.fi FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/device_watcher.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.camera3/stream.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castremotecontrol/remote_control.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castsetup/server.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castwindow/window.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/constants.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/error.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/types.fidl @@ -3263,12 +3266,20 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.legacymetrics/event.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.legacymetrics/metrics_recorder.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/target_discovery.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/activity_reporter.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/profile_provider.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/memorypressure.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session/session_restart_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.settings/night_mode.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/events.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/keyboard.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/modifiers.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.input3/pointer.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.update/update.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.url/url.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.weave/auth.fidl +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/validate_string.cc +FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/commands_sizing.cc +FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/include/lib/ui/scenic/cpp/commands_sizing.h FILE: ../../../fuchsia/sdk/linux/pkg/zx/include/lib/zx/stream.h FILE: ../../../fuchsia/sdk/linux/pkg/zx/stream.cc ---------------------------------------------------------------------------------------------------- diff --git a/ci/licenses_golden/licenses_gpu b/ci/licenses_golden/licenses_gpu new file mode 100644 index 0000000000000..a1f8f8e87988c --- /dev/null +++ b/ci/licenses_golden/licenses_gpu @@ -0,0 +1,42 @@ +Signature: ffe64a3daaf0ad982854594ad155dd56 + +UNUSED LICENSES: + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +USED LICENSES: + +==================================================================================================== +LIBRARY: gpu +ORIGIN: ../../../flutter/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../gpu/GLES2/gl2chromium.h +FILE: ../../../gpu/command_buffer/client/gles2_c_lib_export.h +---------------------------------------------------------------------------------------------------- +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +==================================================================================================== +Total license count: 1 diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 2020d0fbc8a06..8f0b0639ec3f7 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: bc22be57e83e2768786b3add6df2436a +Signature: 2da5a3733cfc3ee9d4098855a1f98d15 UNUSED LICENSES: @@ -486,7 +486,6 @@ FILE: ../../../third_party/skia/gm/hardstop_gradients.cpp FILE: ../../../third_party/skia/gm/imagemakewithfilter.cpp FILE: ../../../third_party/skia/gm/imagemasksubset.cpp FILE: ../../../third_party/skia/gm/lattice.cpp -FILE: ../../../third_party/skia/gm/lightingshader2.cpp FILE: ../../../third_party/skia/gm/overdrawcolorfilter.cpp FILE: ../../../third_party/skia/gm/overstroke.cpp FILE: ../../../third_party/skia/gm/pathmaskcache.cpp @@ -534,7 +533,6 @@ FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_primitive.cpp FILE: ../../../third_party/skia/samplecode/DecodeFile.h FILE: ../../../third_party/skia/samplecode/Sample.cpp FILE: ../../../third_party/skia/samplecode/SampleAndroidShadows.cpp -FILE: ../../../third_party/skia/samplecode/SampleLitAtlas.cpp FILE: ../../../third_party/skia/samplecode/SampleMegaStroke.cpp FILE: ../../../third_party/skia/samplecode/SamplePathOverstroke.cpp FILE: ../../../third_party/skia/samplecode/SampleSVGFile.cpp @@ -571,12 +569,6 @@ FILE: ../../../third_party/skia/src/core/SkLeanWindows.h FILE: ../../../third_party/skia/src/core/SkMSAN.h FILE: ../../../third_party/skia/src/core/SkMatrixPriv.h FILE: ../../../third_party/skia/src/core/SkModeColorFilter.h -FILE: ../../../third_party/skia/src/core/SkNormalFlatSource.cpp -FILE: ../../../third_party/skia/src/core/SkNormalFlatSource.h -FILE: ../../../third_party/skia/src/core/SkNormalMapSource.cpp -FILE: ../../../third_party/skia/src/core/SkNormalMapSource.h -FILE: ../../../third_party/skia/src/core/SkNormalSource.cpp -FILE: ../../../third_party/skia/src/core/SkNormalSource.h FILE: ../../../third_party/skia/src/core/SkOverdrawCanvas.cpp FILE: ../../../third_party/skia/src/core/SkPathMeasurePriv.h FILE: ../../../third_party/skia/src/core/SkRasterPipeline.cpp @@ -641,8 +633,6 @@ FILE: ../../../third_party/skia/src/gpu/GrTextureRenderTargetProxy.h FILE: ../../../third_party/skia/src/gpu/GrUserStencilSettings.h FILE: ../../../third_party/skia/src/gpu/GrWindowRectangles.h FILE: ../../../third_party/skia/src/gpu/GrWindowRectsState.h -FILE: ../../../third_party/skia/src/gpu/effects/GrSRGBEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/GrSRGBEffect.h FILE: ../../../third_party/skia/src/gpu/effects/GrShadowGeoProc.cpp FILE: ../../../third_party/skia/src/gpu/effects/GrShadowGeoProc.h FILE: ../../../third_party/skia/src/gpu/geometry/GrShape.cpp @@ -706,7 +696,6 @@ FILE: ../../../third_party/skia/src/ports/SkImageGeneratorCG.cpp FILE: ../../../third_party/skia/src/ports/SkImageGeneratorWIC.cpp FILE: ../../../third_party/skia/src/shaders/SkColorFilterShader.h FILE: ../../../third_party/skia/src/shaders/SkColorShader.cpp -FILE: ../../../third_party/skia/src/shaders/SkLights.cpp FILE: ../../../third_party/skia/src/shaders/gradients/Sk4fGradientBase.cpp FILE: ../../../third_party/skia/src/shaders/gradients/Sk4fGradientBase.h FILE: ../../../third_party/skia/src/shaders/gradients/Sk4fGradientPriv.h @@ -1012,7 +1001,6 @@ FILE: ../../../third_party/skia/infra/bots/assets/linux_vulkan_sdk/VERSION FILE: ../../../third_party/skia/infra/bots/assets/lottie-samples/VERSION FILE: ../../../third_party/skia/infra/bots/assets/mesa_intel_driver_linux/VERSION FILE: ../../../third_party/skia/infra/bots/assets/mesa_intel_driver_linux/mesa-driver-builder/Dockerfile -FILE: ../../../third_party/skia/infra/bots/assets/moltenvk/VERSION FILE: ../../../third_party/skia/infra/bots/assets/mskp/VERSION FILE: ../../../third_party/skia/infra/bots/assets/node/VERSION FILE: ../../../third_party/skia/infra/bots/assets/opencl_headers/VERSION @@ -1046,51 +1034,48 @@ FILE: ../../../third_party/skia/infra/bots/jobs.json FILE: ../../../third_party/skia/infra/bots/lottie_web.isolate FILE: ../../../third_party/skia/infra/bots/pathkit.isolate FILE: ../../../third_party/skia/infra/bots/perf_skia_bundled.isolate +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-arm-Release-Android_API26.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-arm-Release-Android_ASAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-arm-Release-Chromebook_GLES.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-arm-Release-Flutter_Android.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-arm64-Release-Android_Wuffs.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86-devrel-Android_SKQP.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-Chromebook_GLES.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-Coverage.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-MSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-OpenCL.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-SK_CPU_LIMIT_SSE41.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-SafeStack.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-SwiftShader_MSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-SwiftShader_TSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-TSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-Tidy.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-Wuffs.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-ANGLE.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-ASAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-CMake.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-Fast.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-NoDEPS.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-Static.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-SwiftShader.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Release-Vulkan.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-asmjs-Debug-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-asmjs-Release-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-wasm-Debug-CanvasKit.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-wasm-Debug-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-wasm-Release-CanvasKit_CPU.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-EMCC-wasm-Release-PathKit.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-GCC-loongson3a-Release-Docker.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-GCC-x86-Debug-Docker.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-GCC-x86_64-Debug-Docker.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-GCC-x86_64-Release-NoGPU_Docker.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-GCC-x86_64-Release-Shared_Docker.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-arm-Release-Android_API26.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-arm-Release-Android_ASAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-arm-Release-Chromebook_GLES.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-arm-Release-Flutter_Android.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-arm64-Release-Android_Wuffs.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86-devrel-Android_SKQP.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-Chromebook_GLES.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-Coverage.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-MSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-OpenCL.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SK_CPU_LIMIT_SSE41.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SafeStack.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SkVM.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SkVM_ASAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SwiftShader_MSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-SwiftShader_TSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-TSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-Tidy.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Debug-Wuffs.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-ANGLE.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-ASAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-CMake.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-Fast.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-NoDEPS.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-Static.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-SwiftShader.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-Clang-x86_64-Release-Vulkan.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-asmjs-Debug-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-asmjs-Release-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-CanvasKit.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-CanvasKit_CPU.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-PathKit.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-arm-Debug-iOS.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-arm64-Debug-Android_Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-arm64-Debug-iOS.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-x86_64-Debug-ASAN.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-x86_64-Debug-CommandBuffer.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-x86_64-Debug-Metal.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-x86_64-Release-MoltenVK_Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Win-Clang-arm64-Release-Android.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Win-Clang-x86-Debug-Exceptions.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Win-Clang-x86_64-Debug-ANGLE.json @@ -1100,11 +1085,11 @@ FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.ex FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Win-Clang-x86_64-Release-Shared.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Win-Clang-x86_64-Release-Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Housekeeper-PerCommit-CheckGeneratedFiles.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Test-Debian9-Clang-GCE-CPU-AVX2-universal-devrel-All-Android_SKQP.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Test-Debian10-Clang-GCE-CPU-AVX2-universal-devrel-All-Android_SKQP.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/unknown-docker-image.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/builder_name_schema/builder_name_schema.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/builder_name_schema/examples/full.expected/test.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/checkout/examples/full.expected/Build-Debian9-Clang-x86_64-Release-NoDEPS.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/checkout/examples/full.expected/Build-Debian10-Clang-x86_64-Release-NoDEPS.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/checkout/examples/full.expected/Build-Mac-Clang-x86_64-Debug-CommandBuffer.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/checkout/examples/full.expected/Housekeeper-Weekly-RecreateSKPs.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/checkout/examples/full.expected/cross_repo_trybot.json @@ -1119,8 +1104,8 @@ FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.e FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Android-Clang-Nexus5x-GPU-Adreno418-arm64-Debug-All-Android.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Android-Clang-Pixel-GPU-Adreno530-arm64-Release-All-Android_Skpbench_Mskp.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Debian9-Clang-GCE-CPU-AVX2-x86_64-Debug-All-MSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Debian9-Clang-GCE-CPU-AVX2-x86_64-Release-All-ASAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Debian10-Clang-GCE-CPU-AVX2-x86_64-Debug-All-MSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Debian10-Clang-GCE-CPU-AVX2-x86_64-Release-All-ASAN.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-Win2019-Clang-GCE-CPU-AVX2-x86_64-Debug-All-ASAN.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Android-Clang-AndroidOne-GPU-Mali400MP2-arm-Release-All-Android.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Android-Clang-GalaxyS7_G930FD-GPU-MaliT880-arm64-Debug-All-Android.json @@ -1128,14 +1113,15 @@ FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.e FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Android-Clang-Nexus5x-GPU-Adreno418-arm64-Release-All-Android_ASAN.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Android-Clang-Pixel3a-GPU-Adreno615-arm64-Debug-All-Android_Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-GCE-CPU-AVX2-x86_64-Debug-All-Coverage.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-GCE-CPU-AVX2-x86_64-Release-All-Lottie.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-GCE-CPU-AVX2-x86_64-Release-All-TSAN.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-GCE-GPU-SwiftShader-x86_64-Debug-All-SwiftShader.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-ASAN_Vulkan.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-OpenCL.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-GCC-GCE-CPU-AVX2-x86-Debug-All-Docker.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian10-GCC-GCE-CPU-AVX2-x86_64-Debug-All-Docker.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-GCE-CPU-AVX2-x86_64-Debug-All-Coverage.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-GCE-CPU-AVX2-x86_64-Release-All-Lottie.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-GCE-CPU-AVX2-x86_64-Release-All-TSAN.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-GCE-GPU-SwiftShader-x86_64-Debug-All-SwiftShader.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-OpenCL.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Debian9-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-Vulkan.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Mac10.13-Clang-MacBookPro11.5-CPU-AVX2-x86_64-Debug-All-ASAN.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Valgrind_AbandonGpuContext_SK_CPU_LIMIT_SSE41.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/flavor/examples/full.expected/Test-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Vulkan_ProcDump.json @@ -1158,7 +1144,7 @@ FILE: ../../../third_party/skia/infra/bots/recipe_modules/gsutil/examples/full.e FILE: ../../../third_party/skia/infra/bots/recipe_modules/gsutil/examples/full.expected/gsutil_tests.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/infra/examples/full.expected/infra_tests.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/run/examples/full.expected/test.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/vars/examples/full.expected/Build-Debian9-Clang-x86_64-Release-SKNX_NO_SIMD.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/vars/examples/full.expected/Build-Debian10-Clang-x86_64-Release-SKNX_NO_SIMD.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/vars/examples/full.expected/Housekeeper-Weekly-RecreateSKPs.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/vars/examples/full.expected/integer_issue.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/vars/examples/full.expected/win_test.json @@ -1172,7 +1158,7 @@ FILE: ../../../third_party/skia/infra/bots/recipes/check_generated_files.expecte FILE: ../../../third_party/skia/infra/bots/recipes/compile.expected/Build-Win-Clang-x86-Debug.json FILE: ../../../third_party/skia/infra/bots/recipes/compute_buildstats.expected/normal_bot.json FILE: ../../../third_party/skia/infra/bots/recipes/compute_buildstats.expected/trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/compute_test.expected/Test-Debian9-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-OpenCL.json +FILE: ../../../third_party/skia/infra/bots/recipes/compute_test.expected/Test-Debian10-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Debug-All-OpenCL.json FILE: ../../../third_party/skia/infra/bots/recipes/g3_compile.expected/g3_compile_nontrybot.json FILE: ../../../third_party/skia/infra/bots/recipes/g3_compile.expected/g3_compile_trybot.json FILE: ../../../third_party/skia/infra/bots/recipes/g3_compile.expected/g3_compile_trybot_failure.json @@ -1182,14 +1168,14 @@ FILE: ../../../third_party/skia/infra/bots/recipes/infra.expected/infra_tests.js FILE: ../../../third_party/skia/infra/bots/recipes/perf.expected/Perf-Android-Clang-Nexus7-CPU-Tegra3-arm-Debug-All-Android.json FILE: ../../../third_party/skia/infra/bots/recipes/perf.expected/Perf-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Valgrind_SK_CPU_LIMIT_SSE41.json FILE: ../../../third_party/skia/infra/bots/recipes/perf.expected/Perf-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-ANGLE.json -FILE: ../../../third_party/skia/infra/bots/recipes/perf_canvaskit.expected/Perf-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-CanvasKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/perf_canvaskit.expected/Perf-Debian10-EMCC-GCE-CPU-AVX2-wasm-Release-All-CanvasKit.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_canvaskit.expected/pathkit_trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/perf_pathkit.expected/Perf-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipes/perf_pathkit.expected/Perf-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/perf_pathkit.expected/Perf-Debian10-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/perf_pathkit.expected/Perf-Debian10-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_pathkit.expected/pathkit_trybot.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/Perf-Android-Clang-AndroidOne-GPU-Mali400MP2-arm-Release-All-Android_SkottieTracing.json -FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/Perf-Debian9-Clang-GCE-CPU-AVX2-x86_64-Release-All-SkottieTracing.json -FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/Perf-Debian9-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-SkottieTracing.json +FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/Perf-Debian10-Clang-GCE-CPU-AVX2-x86_64-Release-All-SkottieTracing.json +FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/Perf-Debian10-Clang-NUC7i5BNK-GPU-IntelIris640-x86_64-Release-All-SkottieTracing.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/skottietracing_parse_trace_error.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottietrace.expected/skottietracing_trybot.json FILE: ../../../third_party/skia/infra/bots/recipes/perf_skottiewasm_lottieweb.expected/lottie_web_canvas_perf.json @@ -1208,24 +1194,24 @@ FILE: ../../../third_party/skia/infra/bots/recipes/skpbench.expected/Perf-Androi FILE: ../../../third_party/skia/infra/bots/recipes/skpbench.expected/Perf-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Vulkan_Skpbench.json FILE: ../../../third_party/skia/infra/bots/recipes/skpbench.expected/Perf-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Vulkan_Skpbench_DDLTotal_9x9.json FILE: ../../../third_party/skia/infra/bots/recipes/skpbench.expected/trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/skqp_test.expected/Test-Debian9-Clang-GCE-CPU-AVX2-universal-devrel-All-Android_SKQP.json -FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Debian9-Clang-arm-Release-Flutter_Android.json -FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Debian9-Clang-universal-devrel-Android_SKQP.json +FILE: ../../../third_party/skia/infra/bots/recipes/skqp_test.expected/Test-Debian10-Clang-GCE-CPU-AVX2-universal-devrel-All-Android_SKQP.json +FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Debian10-Clang-arm-Release-Flutter_Android.json +FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Debian10-Clang-universal-devrel-Android_SKQP.json FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Mac-Clang-x86_64-Debug-CommandBuffer.json FILE: ../../../third_party/skia/infra/bots/recipes/sync_and_compile.expected/Build-Win10-Clang-x86_64-Release-NoDEPS.json FILE: ../../../third_party/skia/infra/bots/recipes/test.expected/Test-Android-Clang-Pixel-GPU-Adreno530-arm-Debug-All-Android_ASAN.json FILE: ../../../third_party/skia/infra/bots/recipes/test.expected/Test-Android-Clang-Pixel2XL-GPU-Adreno540-arm64-Debug-All-Android.json -FILE: ../../../third_party/skia/infra/bots/recipes/test.expected/Test-Debian9-Clang-GCE-CPU-AVX2-x86_64-Release-All-Lottie.json +FILE: ../../../third_party/skia/infra/bots/recipes/test.expected/Test-Debian10-Clang-GCE-CPU-AVX2-x86_64-Release-All-Lottie.json FILE: ../../../third_party/skia/infra/bots/recipes/test.expected/Test-Win10-Clang-ShuttleC-GPU-GTX960-x86_64-Debug-All-ANGLE.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_canvaskit.expected/Test-Debian9-EMCC-GCE-GPU-WEBGL1-wasm-Debug-All-CanvasKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_canvaskit.expected/Test-Debian10-EMCC-GCE-GPU-WEBGL1-wasm-Debug-All-CanvasKit.json FILE: ../../../third_party/skia/infra/bots/recipes/test_canvaskit.expected/canvaskit_trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_lottie_web.expected/Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_lottie_web.expected/Test-Debian10-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb.json FILE: ../../../third_party/skia/infra/bots/recipes/test_lottie_web.expected/lottie_web_trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian10-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian10-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/Test-Debian10-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json FILE: ../../../third_party/skia/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json -FILE: ../../../third_party/skia/infra/bots/recipes/test_skqp_emulator.expected/Test-Debian9-Clang-GCE-CPU-Emulator-x86-devrel-All-Android_SKQP.json +FILE: ../../../third_party/skia/infra/bots/recipes/test_skqp_emulator.expected/Test-Debian10-Clang-GCE-CPU-Emulator-x86-devrel-All-Android_SKQP.json FILE: ../../../third_party/skia/infra/bots/recipes/upload_buildstats_results.expected/normal_bot.json FILE: ../../../third_party/skia/infra/bots/recipes/upload_buildstats_results.expected/trybot.json FILE: ../../../third_party/skia/infra/bots/recipes/upload_dm_results.expected/alternate_bucket.json @@ -1298,6 +1284,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/perf/assets/confetti.json FILE: ../../../third_party/skia/modules/canvaskit/perf/assets/drinks.json FILE: ../../../third_party/skia/modules/canvaskit/perf/assets/lego_loader.json FILE: ../../../third_party/skia/modules/canvaskit/perf/assets/onboarding.json +FILE: ../../../third_party/skia/modules/canvaskit/perf/matrix.bench.js FILE: ../../../third_party/skia/modules/canvaskit/postamble.js FILE: ../../../third_party/skia/modules/canvaskit/preamble.js FILE: ../../../third_party/skia/modules/canvaskit/ready.js @@ -2270,7 +2257,6 @@ FILE: ../../../third_party/skia/gm/drawatlascolor.cpp FILE: ../../../third_party/skia/gm/drawminibitmaprect.cpp FILE: ../../../third_party/skia/gm/fadefilter.cpp FILE: ../../../third_party/skia/gm/fontscalerdistortable.cpp -FILE: ../../../third_party/skia/gm/gamma.cpp FILE: ../../../third_party/skia/gm/image_pict.cpp FILE: ../../../third_party/skia/gm/image_shader.cpp FILE: ../../../third_party/skia/gm/imagefilters.cpp @@ -2282,7 +2268,6 @@ FILE: ../../../third_party/skia/gm/imagesource2.cpp FILE: ../../../third_party/skia/gm/largeglyphblur.cpp FILE: ../../../third_party/skia/gm/lcdblendmodes.cpp FILE: ../../../third_party/skia/gm/lcdoverlap.cpp -FILE: ../../../third_party/skia/gm/lightingshader.cpp FILE: ../../../third_party/skia/gm/localmatriximagefilter.cpp FILE: ../../../third_party/skia/gm/localmatriximageshader.cpp FILE: ../../../third_party/skia/gm/mipmap.cpp @@ -2342,7 +2327,6 @@ FILE: ../../../third_party/skia/samplecode/SampleAnimatedText.cpp FILE: ../../../third_party/skia/samplecode/SampleAtlas.cpp FILE: ../../../third_party/skia/samplecode/SampleClipDrawMatch.cpp FILE: ../../../third_party/skia/samplecode/SampleFilterQuality.cpp -FILE: ../../../third_party/skia/samplecode/SampleLighting.cpp FILE: ../../../third_party/skia/samplecode/SampleShip.cpp FILE: ../../../third_party/skia/samplecode/SampleXfer.cpp FILE: ../../../third_party/skia/src/android/SkBitmapRegionCodec.cpp @@ -2441,7 +2425,6 @@ FILE: ../../../third_party/skia/src/gpu/GrDrawingManager.h FILE: ../../../third_party/skia/src/gpu/GrFragmentProcessor.cpp FILE: ../../../third_party/skia/src/gpu/GrGpuResourcePriv.h FILE: ../../../third_party/skia/src/gpu/GrManagedResource.h -FILE: ../../../third_party/skia/src/gpu/GrMesh.h FILE: ../../../third_party/skia/src/gpu/GrNonAtomicRef.h FILE: ../../../third_party/skia/src/gpu/GrOpFlushState.cpp FILE: ../../../third_party/skia/src/gpu/GrOpFlushState.h @@ -2454,10 +2437,11 @@ FILE: ../../../third_party/skia/src/gpu/GrRenderTargetPriv.h FILE: ../../../third_party/skia/src/gpu/GrResourceProvider.cpp FILE: ../../../third_party/skia/src/gpu/GrResourceProvider.h FILE: ../../../third_party/skia/src/gpu/GrSamplerState.h -FILE: ../../../third_party/skia/src/gpu/GrTessellator.cpp -FILE: ../../../third_party/skia/src/gpu/GrTessellator.h +FILE: ../../../third_party/skia/src/gpu/GrSimpleMesh.h FILE: ../../../third_party/skia/src/gpu/GrTestUtils.cpp FILE: ../../../third_party/skia/src/gpu/GrTestUtils.h +FILE: ../../../third_party/skia/src/gpu/GrTriangulator.cpp +FILE: ../../../third_party/skia/src/gpu/GrTriangulator.h FILE: ../../../third_party/skia/src/gpu/GrXferProcessor.cpp FILE: ../../../third_party/skia/src/gpu/GrYUVProvider.cpp FILE: ../../../third_party/skia/src/gpu/GrYUVProvider.h @@ -2479,8 +2463,6 @@ FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLProgramBuilder.cpp FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLProgramBuilder.h FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLProgramDataManager.h FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLUniformHandler.h -FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLUtil.cpp -FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLUtil.h FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLVarying.cpp FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLVarying.h FILE: ../../../third_party/skia/src/gpu/glsl/GrGLSLXferProcessor.h @@ -2507,8 +2489,8 @@ FILE: ../../../third_party/skia/src/gpu/ops/GrMeshDrawOp.h FILE: ../../../third_party/skia/src/gpu/ops/GrOp.cpp FILE: ../../../third_party/skia/src/gpu/ops/GrOp.h FILE: ../../../third_party/skia/src/gpu/ops/GrStencilPathOp.h -FILE: ../../../third_party/skia/src/gpu/ops/GrTessellatingPathRenderer.cpp -FILE: ../../../third_party/skia/src/gpu/ops/GrTessellatingPathRenderer.h +FILE: ../../../third_party/skia/src/gpu/ops/GrTriangulatingPathRenderer.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrTriangulatingPathRenderer.h FILE: ../../../third_party/skia/src/gpu/text/GrDistanceFieldAdjustTable.cpp FILE: ../../../third_party/skia/src/gpu/text/GrDistanceFieldAdjustTable.h FILE: ../../../third_party/skia/src/gpu/text/GrStrikeCache.cpp @@ -2530,12 +2512,12 @@ FILE: ../../../third_party/skia/src/gpu/vk/GrVkGpu.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkGpu.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkImage.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkImage.h -FILE: ../../../third_party/skia/src/gpu/vk/GrVkIndexBuffer.cpp -FILE: ../../../third_party/skia/src/gpu/vk/GrVkIndexBuffer.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkInterface.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkInterface.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkMemory.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkMemory.h +FILE: ../../../third_party/skia/src/gpu/vk/GrVkMeshBuffer.cpp +FILE: ../../../third_party/skia/src/gpu/vk/GrVkMeshBuffer.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderPass.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderPass.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkRenderTarget.cpp @@ -2552,8 +2534,6 @@ FILE: ../../../third_party/skia/src/gpu/vk/GrVkUniformBuffer.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkUniformBuffer.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkUtil.cpp FILE: ../../../third_party/skia/src/gpu/vk/GrVkUtil.h -FILE: ../../../third_party/skia/src/gpu/vk/GrVkVertexBuffer.cpp -FILE: ../../../third_party/skia/src/gpu/vk/GrVkVertexBuffer.h FILE: ../../../third_party/skia/src/image/SkImage_Gpu.h FILE: ../../../third_party/skia/src/image/SkImage_Lazy.cpp FILE: ../../../third_party/skia/src/opts/Sk4px_NEON.h @@ -2588,9 +2568,6 @@ FILE: ../../../third_party/skia/src/ports/SkOSLibrary_posix.cpp FILE: ../../../third_party/skia/src/ports/SkOSLibrary_win.cpp FILE: ../../../third_party/skia/src/shaders/SkImageShader.cpp FILE: ../../../third_party/skia/src/shaders/SkImageShader.h -FILE: ../../../third_party/skia/src/shaders/SkLightingShader.cpp -FILE: ../../../third_party/skia/src/shaders/SkLightingShader.h -FILE: ../../../third_party/skia/src/shaders/SkLights.h FILE: ../../../third_party/skia/src/svg/SkSVGCanvas.cpp FILE: ../../../third_party/skia/src/svg/SkSVGDevice.cpp FILE: ../../../third_party/skia/src/svg/SkSVGDevice.h @@ -2882,7 +2859,7 @@ FILE: ../../../third_party/skia/include/effects/SkRuntimeEffect.h FILE: ../../../third_party/skia/include/gpu/gl/GrGLAssembleHelpers.h FILE: ../../../third_party/skia/include/private/GrGLTypesPriv.h FILE: ../../../third_party/skia/include/private/SkThreadAnnotations.h -FILE: ../../../third_party/skia/modules/canvaskit/WasmAliases.h +FILE: ../../../third_party/skia/modules/canvaskit/WasmCommon.h FILE: ../../../third_party/skia/modules/canvaskit/paragraph_bindings.cpp FILE: ../../../third_party/skia/modules/canvaskit/particles_bindings.cpp FILE: ../../../third_party/skia/modules/canvaskit/skottie_bindings.cpp @@ -3001,6 +2978,8 @@ FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoDecoder.cpp FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoDecoder.h FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoEncoder.cpp FILE: ../../../third_party/skia/experimental/ffmpeg/SkVideoEncoder.h +FILE: ../../../third_party/skia/experimental/svg/model/SkSVGText.cpp +FILE: ../../../third_party/skia/experimental/svg/model/SkSVGText.h FILE: ../../../third_party/skia/gm/backdrop.cpp FILE: ../../../third_party/skia/gm/backdrop_imagefilter_croprect.cpp FILE: ../../../third_party/skia/gm/bug9331.cpp @@ -3086,7 +3065,6 @@ FILE: ../../../third_party/skia/src/core/SkStrikeForGPU.cpp FILE: ../../../third_party/skia/src/core/SkStrikeForGPU.h FILE: ../../../third_party/skia/src/core/SkStrikeSpec.h FILE: ../../../third_party/skia/src/core/SkVMBlitter.cpp -FILE: ../../../third_party/skia/src/core/SkVMBlitter.h FILE: ../../../third_party/skia/src/core/SkYUVMath.cpp FILE: ../../../third_party/skia/src/core/SkYUVMath.h FILE: ../../../third_party/skia/src/core/SkZip.h @@ -3131,8 +3109,6 @@ FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRenderTarget.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRenderTarget.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRingBuffer.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnRingBuffer.h -FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStagingManager.cpp -FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStagingManager.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStencilAttachment.cpp FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStencilAttachment.h FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTexture.cpp @@ -3251,7 +3227,6 @@ FILE: ../../../third_party/skia/gm/scaledemoji.cpp FILE: ../../../third_party/skia/gm/scaledemoji_rendering.cpp FILE: ../../../third_party/skia/gm/shadermaskfilter.cpp FILE: ../../../third_party/skia/gm/sharedcorners.cpp -FILE: ../../../third_party/skia/gm/skinning.cpp FILE: ../../../third_party/skia/gm/trickycubicstrokes.cpp FILE: ../../../third_party/skia/gm/unpremul.cpp FILE: ../../../third_party/skia/gm/wacky_yuv_formats.cpp @@ -3516,6 +3491,7 @@ FILE: ../../../third_party/skia/src/pdf/SkClusterator.h FILE: ../../../third_party/skia/src/pdf/SkPDFTag.cpp FILE: ../../../third_party/skia/src/pdf/SkPDFTag.h FILE: ../../../third_party/skia/src/ports/SkFontMgr_fuchsia.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLByteCode.cpp FILE: ../../../third_party/skia/src/sksl/SkSLCPPUniformCTypes.cpp FILE: ../../../third_party/skia/src/sksl/SkSLCPPUniformCTypes.h FILE: ../../../third_party/skia/src/sksl/SkSLPipelineStageCodeGenerator.cpp @@ -3648,7 +3624,6 @@ FILE: ../../../third_party/skia/modules/skottie/gm/ExternalProperties.cpp FILE: ../../../third_party/skia/modules/skottie/gm/SkottieGM.cpp FILE: ../../../third_party/skia/modules/skottie/include/Skottie.h FILE: ../../../third_party/skia/modules/skottie/src/Skottie.cpp -FILE: ../../../third_party/skia/modules/skottie/src/SkottieValue.cpp FILE: ../../../third_party/skia/modules/skottie/src/SkottieValue.h FILE: ../../../third_party/skia/modules/sksg/include/SkSGDraw.h FILE: ../../../third_party/skia/modules/sksg/include/SkSGEffectNode.h @@ -3899,30 +3874,49 @@ TYPE: LicenseType.bsd FILE: ../../../third_party/skia/bench/GrQuadBench.cpp FILE: ../../../third_party/skia/gm/crbug_1041204.cpp FILE: ../../../third_party/skia/gm/crbug_224618.cpp -FILE: ../../../third_party/skia/include/gpu/d3d/GrD3D12.h FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DBackendContext.h +FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypes.h +FILE: ../../../third_party/skia/include/gpu/d3d/GrD3DTypesMinimal.h +FILE: ../../../third_party/skia/include/private/GrD3DTypesPriv.h FILE: ../../../third_party/skia/include/private/SkIDChangeListener.h +FILE: ../../../third_party/skia/modules/canvaskit/viewer_bindings.cpp FILE: ../../../third_party/skia/src/core/SkIDChangeListener.cpp FILE: ../../../third_party/skia/src/core/SkVM_fwd.h +FILE: ../../../third_party/skia/src/gpu/GrBlockAllocator.cpp +FILE: ../../../third_party/skia/src/gpu/GrBlockAllocator.h FILE: ../../../third_party/skia/src/gpu/GrManagedResource.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DBuffer.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCaps.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCaps.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCommandList.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DCommandList.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DGpu.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DGpu.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DOpsRenderPass.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DOpsRenderPass.h -FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DResource.cpp -FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DResource.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DPipelineState.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DPipelineState.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DRenderTarget.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DRenderTarget.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DResourceProvider.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DResourceProvider.h FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DResourceState.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DStencilAttachment.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DStencilAttachment.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTexture.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTexture.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTextureRenderTarget.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTextureRenderTarget.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTextureResource.cpp +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTextureResource.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTypesPriv.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DUtil.cpp FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DUtil.h FILE: ../../../third_party/skia/src/gpu/effects/GrDeviceSpaceEffect.fp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.cpp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrDeviceSpaceEffect.h FILE: ../../../third_party/skia/src/gpu/vk/GrVkManagedResource.h -FILE: ../../../third_party/skia/src/sksl/SkSLInterpreter.h FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.cpp FILE: ../../../third_party/skia/src/sksl/SkSLSPIRVtoHLSL.h ---------------------------------------------------------------------------------------------------- @@ -5063,12 +5057,12 @@ FILE: ../../../third_party/skia/src/gpu/ccpr/GrStencilAtlasOp.h FILE: ../../../third_party/skia/src/gpu/effects/GrComposeLerpEffect.fp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.cpp FILE: ../../../third_party/skia/src/gpu/effects/generated/GrComposeLerpEffect.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrGpuTessellationPathRenderer.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrGpuTessellationPathRenderer.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrStencilPathShader.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellatePathOp.h +FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessellationPathRenderer.h FILE: ../../../third_party/skia/src/pdf/SkPDFGraphicStackState.cpp FILE: ../../../third_party/skia/src/pdf/SkPDFGraphicStackState.h FILE: ../../../third_party/skia/src/pdf/SkPDFType1Font.cpp @@ -5168,10 +5162,12 @@ TYPE: LicenseType.bsd FILE: ../../../third_party/skia/docs/examples/50_percent_gray.cpp FILE: ../../../third_party/skia/docs/examples/50_percent_srgb.cpp FILE: ../../../third_party/skia/docs/examples/BlendModes.cpp +FILE: ../../../third_party/skia/docs/examples/Canvas_saveLayer_4.cpp FILE: ../../../third_party/skia/docs/examples/ChromeMDRefreshTab.cpp FILE: ../../../third_party/skia/docs/examples/ChromeMDRefreshTabs.cpp FILE: ../../../third_party/skia/docs/examples/Color_Wheel.cpp FILE: ../../../third_party/skia/docs/examples/DCIToXYZD50.cpp +FILE: ../../../third_party/skia/docs/examples/GradientShader_MakeLinear.cpp FILE: ../../../third_party/skia/docs/examples/Octopus_Generator.cpp FILE: ../../../third_party/skia/docs/examples/Octopus_Generator_Animated.cpp FILE: ../../../third_party/skia/docs/examples/PaintDump.cpp @@ -5596,13 +5592,22 @@ FILE: ../../../third_party/skia/gm/verifiers/gmverifier.cpp FILE: ../../../third_party/skia/gm/verifiers/gmverifier.h FILE: ../../../third_party/skia/include/core/SkM44.h FILE: ../../../third_party/skia/modules/skottie/src/Adapter.h -FILE: ../../../third_party/skia/modules/skottie/src/Animator.cpp -FILE: ../../../third_party/skia/modules/skottie/src/Animator.h FILE: ../../../third_party/skia/modules/skottie/src/Camera.cpp FILE: ../../../third_party/skia/modules/skottie/src/Camera.h FILE: ../../../third_party/skia/modules/skottie/src/Path.cpp FILE: ../../../third_party/skia/modules/skottie/src/Transform.cpp FILE: ../../../third_party/skia/modules/skottie/src/Transform.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Animator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/KeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/animator/ScalarKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/ShapeKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/TextKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/Vec2KeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.cpp +FILE: ../../../third_party/skia/modules/skottie/src/animator/VectorKeyframeAnimator.h +FILE: ../../../third_party/skia/modules/skottie/src/effects/CornerPinEffect.cpp FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Ellipse.cpp FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/FillStroke.cpp FILE: ../../../third_party/skia/modules/skottie/src/layers/shapelayer/Gradient.cpp @@ -5618,6 +5623,7 @@ FILE: ../../../third_party/skia/modules/sksg/src/SkSGDashEffect.cpp FILE: ../../../third_party/skia/modules/skshaper/src/SkShaper_coretext.cpp FILE: ../../../third_party/skia/samplecode/Sample3D.cpp FILE: ../../../third_party/skia/src/core/SkCanvasMatrix.h +FILE: ../../../third_party/skia/src/core/SkColorFilterPriv.h FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.cpp FILE: ../../../third_party/skia/src/core/SkCompressedDataUtils.h FILE: ../../../third_party/skia/src/core/SkM44.cpp @@ -5625,6 +5631,11 @@ FILE: ../../../third_party/skia/src/core/SkVerticesPriv.h FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.cpp FILE: ../../../third_party/skia/src/gpu/GrDynamicAtlas.h FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.h +FILE: ../../../third_party/skia/src/gpu/GrStagingBuffer.cpp +FILE: ../../../third_party/skia/src/gpu/GrStagingBuffer.h +FILE: ../../../third_party/skia/src/gpu/ccpr/GrAutoMapVertexBuffer.h +FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStagingBuffer.cpp +FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnStagingBuffer.h FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.cpp FILE: ../../../third_party/skia/src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h FILE: ../../../third_party/skia/src/gpu/tessellate/GrDrawAtlasPathOp.cpp @@ -6120,7 +6131,6 @@ FILE: ../../../third_party/skia/src/core/SkImageInfo.cpp FILE: ../../../third_party/skia/src/core/SkRasterClip.cpp FILE: ../../../third_party/skia/src/core/SkRasterClip.h FILE: ../../../third_party/skia/src/core/SkStrikeCache.h -FILE: ../../../third_party/skia/src/gpu/GrAllocator.h FILE: ../../../third_party/skia/src/gpu/GrBufferAllocPool.cpp FILE: ../../../third_party/skia/src/gpu/GrBufferAllocPool.h FILE: ../../../third_party/skia/src/gpu/GrClip.h @@ -6128,6 +6138,7 @@ FILE: ../../../third_party/skia/src/gpu/GrColor.h FILE: ../../../third_party/skia/src/gpu/GrFixedClip.cpp FILE: ../../../third_party/skia/src/gpu/GrGlyph.h FILE: ../../../third_party/skia/src/gpu/GrGpu.cpp +FILE: ../../../third_party/skia/src/gpu/GrTAllocator.h FILE: ../../../third_party/skia/src/gpu/GrVertexWriter.h FILE: ../../../third_party/skia/src/gpu/SkGpuDevice.h FILE: ../../../third_party/skia/src/gpu/SkGr.cpp @@ -6359,6 +6370,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/example.html FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/extra.html FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/node.example.js FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/package.json +FILE: ../../../third_party/skia/modules/canvaskit/canvaskit/viewer.html FILE: ../../../third_party/skia/modules/pathkit/npm-asmjs/example.html FILE: ../../../third_party/skia/modules/pathkit/npm-asmjs/package.json FILE: ../../../third_party/skia/modules/pathkit/npm-wasm/example.html diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index 623596773cb06..18cdd40a7c8e2 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: b2a998d040384765d4bf2b5aa6e318ef +Signature: cdaa56a5f77c8a504af3e424af40fb9f UNUSED LICENSES: @@ -542,6 +542,7 @@ POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== LIBRARY: angle LIBRARY: boringssl +LIBRARY: khronos LIBRARY: observatory_pub_packages LIBRARY: vulkan LIBRARY: wuffs @@ -664,6 +665,8 @@ FILE: ../../../third_party/dart/third_party/observatory_pub_packages/packages/qu FILE: ../../../third_party/dart/third_party/observatory_pub_packages/packages/quiver/lib/src/time/util.dart FILE: ../../../third_party/dart/third_party/observatory_pub_packages/packages/quiver/lib/strings.dart FILE: ../../../third_party/dart/third_party/observatory_pub_packages/packages/quiver/lib/time.dart +FILE: ../../../third_party/khronos/GLES2/gl2platform.h +FILE: ../../../third_party/khronos/GLES3/gl3platform.h FILE: ../../../third_party/vulkan/include/vulkan/vk_platform.h FILE: ../../../third_party/vulkan/include/vulkan/vulkan.h FILE: ../../../third_party/vulkan/include/vulkan/vulkan_android.h @@ -1016,6 +1019,138 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================================================== +==================================================================================================== +LIBRARY: angle +LIBRARY: khronos +ORIGIN: ../../../third_party/angle/include/EGL/egl.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/angle/include/EGL/egl.h +FILE: ../../../third_party/angle/include/EGL/eglext.h +FILE: ../../../third_party/angle/include/GLES/glext.h +FILE: ../../../third_party/angle/include/GLES2/gl2.h +FILE: ../../../third_party/angle/scripts/egl.xml +FILE: ../../../third_party/khronos/EGL/egl.h +FILE: ../../../third_party/khronos/EGL/eglext.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2013-2017 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + +==================================================================================================== +LIBRARY: angle +LIBRARY: khronos +ORIGIN: ../../../third_party/angle/include/EGL/eglplatform.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/angle/include/EGL/eglplatform.h +FILE: ../../../third_party/khronos/EGL/eglplatform.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2007-2016 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + +==================================================================================================== +LIBRARY: angle +LIBRARY: khronos +ORIGIN: ../../../third_party/angle/include/GLES/gl.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/angle/include/GLES/gl.h +FILE: ../../../third_party/angle/include/GLES2/gl2ext.h +FILE: ../../../third_party/angle/include/GLES3/gl3.h +FILE: ../../../third_party/angle/include/WGL/wgl.h +FILE: ../../../third_party/khronos/GLES2/gl2.h +FILE: ../../../third_party/khronos/GLES2/gl2ext.h +FILE: ../../../third_party/khronos/GLES3/gl3.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2013-2018 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + +==================================================================================================== +LIBRARY: angle +LIBRARY: khronos +ORIGIN: ../../../third_party/angle/include/GLES3/gl31.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/angle/include/GLES3/gl31.h +FILE: ../../../third_party/angle/include/GLES3/gl32.h +FILE: ../../../third_party/khronos/GLES3/gl31.h +FILE: ../../../third_party/khronos/GLES3/gl32.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2013-2016 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + ==================================================================================================== LIBRARY: angle LIBRARY: vulkan-validation-layers @@ -1947,126 +2082,6 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: angle -ORIGIN: ../../../third_party/angle/include/EGL/egl.h -TYPE: LicenseType.unknown -FILE: ../../../third_party/angle/include/EGL/egl.h -FILE: ../../../third_party/angle/include/EGL/eglext.h -FILE: ../../../third_party/angle/include/GLES/glext.h -FILE: ../../../third_party/angle/include/GLES2/gl2.h -FILE: ../../../third_party/angle/scripts/egl.xml ----------------------------------------------------------------------------------------------------- -Copyright (c) 2013-2017 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. -==================================================================================================== - -==================================================================================================== -LIBRARY: angle -ORIGIN: ../../../third_party/angle/include/EGL/eglplatform.h -TYPE: LicenseType.unknown -FILE: ../../../third_party/angle/include/EGL/eglplatform.h ----------------------------------------------------------------------------------------------------- -Copyright (c) 2007-2016 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. -==================================================================================================== - -==================================================================================================== -LIBRARY: angle -ORIGIN: ../../../third_party/angle/include/GLES/gl.h -TYPE: LicenseType.unknown -FILE: ../../../third_party/angle/include/GLES/gl.h -FILE: ../../../third_party/angle/include/GLES2/gl2ext.h -FILE: ../../../third_party/angle/include/GLES3/gl3.h -FILE: ../../../third_party/angle/include/WGL/wgl.h ----------------------------------------------------------------------------------------------------- -Copyright (c) 2013-2018 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. -==================================================================================================== - -==================================================================================================== -LIBRARY: angle -ORIGIN: ../../../third_party/angle/include/GLES3/gl31.h -TYPE: LicenseType.unknown -FILE: ../../../third_party/angle/include/GLES3/gl31.h -FILE: ../../../third_party/angle/include/GLES3/gl32.h ----------------------------------------------------------------------------------------------------- -Copyright (c) 2013-2016 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. -==================================================================================================== - ==================================================================================================== LIBRARY: angle ORIGIN: ../../../third_party/angle/include/GLSLANG/ShaderLang.h + ../../../third_party/angle/LICENSE @@ -7421,6 +7436,12 @@ FILE: ../../../third_party/dart/benchmarks/IsolateJson/dart/sample.json FILE: ../../../third_party/dart/benchmarks/IsolateSpawn/dart/helloworld.dart FILE: ../../../third_party/dart/benchmarks/IsolateSpawnMemory/dart/helloworld.dart FILE: ../../../third_party/dart/benchmarks/SoundSplayTreeSieve/dart/iterable.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/datext_latin1_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/entext_ascii_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/netext_3_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/rutext_2_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/sktext_10k.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/zhtext_10k.dart FILE: ../../../third_party/dart/client/idea/.idea/.name FILE: ../../../third_party/dart/client/idea/.idea/inspectionProfiles/Project_Default.xml FILE: ../../../third_party/dart/client/idea/.idea/vcs.xml @@ -7740,6 +7761,7 @@ ORIGIN: ../../../third_party/dart/benchmarks/ListCopy/dart/ListCopy.dart + ../.. TYPE: LicenseType.bsd FILE: ../../../third_party/dart/benchmarks/ListCopy/dart/ListCopy.dart FILE: ../../../third_party/dart/benchmarks/TypedDataDuplicate/dart/TypedDataDuplicate.dart +FILE: ../../../third_party/dart/benchmarks/Utf8Decode/dart/Utf8Decode.dart FILE: ../../../third_party/dart/runtime/bin/dartdev_utils.cc FILE: ../../../third_party/dart/runtime/bin/dartdev_utils.h FILE: ../../../third_party/dart/runtime/bin/exe_utils.cc @@ -7751,6 +7773,7 @@ FILE: ../../../third_party/dart/runtime/tools/wiki/xref_extractor/lib/xref_extra FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.cc FILE: ../../../third_party/dart/runtime/vm/compiler/aot/dispatch_table_generator.h FILE: ../../../third_party/dart/runtime/vm/compiler/assembler/assembler_base.h +FILE: ../../../third_party/dart/runtime/vm/compiler/backend/reachability_fence_test.cc FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.cc FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/abi.h FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/call.cc @@ -7769,6 +7792,7 @@ FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.cc FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/native_type.h FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.cc FILE: ../../../third_party/dart/runtime/vm/compiler/ffi/recognized_method.h +FILE: ../../../third_party/dart/runtime/vm/compiler/stub_code_compiler.cc FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.cc FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination.h FILE: ../../../third_party/dart/runtime/vm/compiler/write_barrier_elimination_test.cc @@ -7778,14 +7802,23 @@ FILE: ../../../third_party/dart/runtime/vm/dispatch_table.h FILE: ../../../third_party/dart/runtime/vm/field_table.cc FILE: ../../../third_party/dart/runtime/vm/field_table.h FILE: ../../../third_party/dart/runtime/vm/timeline_macos.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test.h +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_arm64.cc +FILE: ../../../third_party/dart/runtime/vm/type_testing_stubs_test_x64.cc FILE: ../../../third_party/dart/runtime/vm/visitor.cc FILE: ../../../third_party/dart/samples/ffi/async/async_test.dart FILE: ../../../third_party/dart/samples/ffi/async/sample_async_callback.dart FILE: ../../../third_party/dart/samples/ffi/async/sample_native_port_call.dart +FILE: ../../../third_party/dart/sdk/lib/_http/embedder_config.dart FILE: ../../../third_party/dart/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart FILE: ../../../third_party/dart/sdk/lib/_internal/js_runtime/lib/js_patch.dart +FILE: ../../../third_party/dart/sdk/lib/_internal/vm/lib/ffi_struct_patch.dart +FILE: ../../../third_party/dart/sdk_nnbd/lib/_http/embedder_config.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_dev_runtime/patch/js_patch.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/js_patch.dart +FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/vm/lib/ffi_struct_patch.dart ---------------------------------------------------------------------------------------------------- Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file for details. All rights reserved. @@ -8281,6 +8314,7 @@ FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/js_array.d FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/js_number.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/js_string.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/math_patch.dart +FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/mirrors_patch_cfe.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/native_helper.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/regexp_helper.dart FILE: ../../../third_party/dart/sdk_nnbd/lib/_internal/js_runtime/lib/string_helper.dart @@ -17649,6 +17683,8 @@ LIBRARY: khronos ORIGIN: ../../../third_party/angle/src/third_party/khronos/GL/wglext.h TYPE: LicenseType.unknown FILE: ../../../third_party/angle/src/third_party/khronos/GL/wglext.h +FILE: ../../../third_party/khronos/noninclude/GL/glext.h +FILE: ../../../third_party/khronos/noninclude/GL/wglext.h ---------------------------------------------------------------------------------------------------- Copyright (c) 2013-2014 The Khronos Group Inc. @@ -17672,6 +17708,117 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. ==================================================================================================== +==================================================================================================== +LIBRARY: khronos +ORIGIN: ../../../third_party/khronos/KHR/khrplatform.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/khronos/KHR/khrplatform.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2008-2009 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + +==================================================================================================== +LIBRARY: khronos +ORIGIN: ../../../third_party/khronos/LICENSE +TYPE: LicenseType.mit +FILE: ../../../third_party/khronos/DEPS +---------------------------------------------------------------------------------------------------- +Copyright (c) 2007-2010 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. + +SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008) + +Copyright (C) 1992 Silicon Graphics, Inc. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice including the dates of first publication and either +this permission notice or a reference to http://oss.sgi.com/projects/FreeB +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON +GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Silicon Graphics, Inc. shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Silicon +Graphics, Inc. +==================================================================================================== + +==================================================================================================== +LIBRARY: khronos +ORIGIN: ../../../third_party/khronos/noninclude/GL/glxext.h +TYPE: LicenseType.unknown +FILE: ../../../third_party/khronos/noninclude/GL/glxext.h +---------------------------------------------------------------------------------------------------- +Copyright (c) 2007-2012 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +==================================================================================================== + ==================================================================================================== LIBRARY: libXNVCtrl ORIGIN: ../../../third_party/angle/src/third_party/libXNVCtrl/LICENSE @@ -23607,4 +23754,4 @@ freely, subject to the following restrictions: misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ==================================================================================================== -Total license count: 367 +Total license count: 370 diff --git a/common/config.gni b/common/config.gni index 1b7d18f670ccd..a9274612223cd 100644 --- a/common/config.gni +++ b/common/config.gni @@ -12,12 +12,8 @@ if (target_cpu == "arm" || target_cpu == "arm64") { import("//build/config/arm.gni") } -if (is_ios || is_mac) { - if (using_fuchsia_sdk) { - import("//build/toolchain/clang.gni") - } else { - import("//build/toolchain/clang_toolchain.gni") - } +if (is_fuchsia) { + import("//build/toolchain/clang.gni") } declare_args() { @@ -64,10 +60,19 @@ if (flutter_runtime_mode == "debug") { feature_defines_list += [ "FLUTTER_RUNTIME_MODE=0" ] } -if (is_fuchsia && using_fuchsia_sdk) { +if (is_fuchsia) { + # TODO(chinmaygarde): This is always set. The macro may now be removed. feature_defines_list += [ "FUCHSIA_SDK=1" ] } if ((is_ios || is_mac) && defined(enable_bitcode)) { flutter_enable_bitcode = enable_bitcode } + +if (is_ios || is_mac) { + flutter_cflags_objc = [ + "-Werror=overriding-method-mismatch", + "-Werror=undeclared-selector", + ] + flutter_cflags_objcc = flutter_cflags_objc +} diff --git a/common/fuchsia_config.gni b/common/fuchsia_config.gni index c8c55b59c5cf2..96e120fba29d7 100644 --- a/common/fuchsia_config.gni +++ b/common/fuchsia_config.gni @@ -4,7 +4,7 @@ import("config.gni") -if (is_fuchsia && using_fuchsia_sdk) { +if (is_fuchsia) { dart_tools_root = "//flutter/tools/fuchsia/dart" fuchsia_base = "//flutter/shell/platform/fuchsia" flutter_runner_base = "$fuchsia_base/flutter/" diff --git a/common/settings.h b/common/settings.h index 1eddf4c16fead..45f851d3543a8 100644 --- a/common/settings.h +++ b/common/settings.h @@ -91,6 +91,7 @@ struct Settings { bool enable_checked_mode = false; bool start_paused = false; bool trace_skia = false; + std::string trace_whitelist; bool trace_startup = false; bool trace_systrace = false; bool dump_skp_on_shader_compilation = false; @@ -98,6 +99,10 @@ struct Settings { bool endless_trace_buffer = false; bool enable_dart_profiling = false; bool disable_dart_asserts = false; + + // Used to signal the embedder whether HTTP connections are disabled. + bool disable_http = false; + // Used as the script URI in debug messages. Does not affect how the Dart code // is executed. std::string advisory_script_uri = "main.dart"; diff --git a/common/task_runners.cc b/common/task_runners.cc index baa0d2b132c91..867fce72e3a11 100644 --- a/common/task_runners.cc +++ b/common/task_runners.cc @@ -10,12 +10,12 @@ namespace flutter { TaskRunners::TaskRunners(std::string label, fml::RefPtr platform, - fml::RefPtr gpu, + fml::RefPtr raster, fml::RefPtr ui, fml::RefPtr io) : label_(std::move(label)), platform_(std::move(platform)), - gpu_(std::move(gpu)), + raster_(std::move(raster)), ui_(std::move(ui)), io_(std::move(io)) {} @@ -39,12 +39,12 @@ fml::RefPtr TaskRunners::GetIOTaskRunner() const { return io_; } -fml::RefPtr TaskRunners::GetGPUTaskRunner() const { - return gpu_; +fml::RefPtr TaskRunners::GetRasterTaskRunner() const { + return raster_; } bool TaskRunners::IsValid() const { - return platform_ && gpu_ && ui_ && io_; + return platform_ && raster_ && ui_ && io_; } } // namespace flutter diff --git a/common/task_runners.h b/common/task_runners.h index 6e08abcad1d1d..291372b785325 100644 --- a/common/task_runners.h +++ b/common/task_runners.h @@ -16,7 +16,7 @@ class TaskRunners { public: TaskRunners(std::string label, fml::RefPtr platform, - fml::RefPtr gpu, + fml::RefPtr raster, fml::RefPtr ui, fml::RefPtr io); @@ -32,14 +32,14 @@ class TaskRunners { fml::RefPtr GetIOTaskRunner() const; - fml::RefPtr GetGPUTaskRunner() const; + fml::RefPtr GetRasterTaskRunner() const; bool IsValid() const; private: const std::string label_; fml::RefPtr platform_; - fml::RefPtr gpu_; + fml::RefPtr raster_; fml::RefPtr ui_; fml::RefPtr io_; }; diff --git a/e2etests/web/regular_integration_tests/README.md b/e2etests/web/regular_integration_tests/README.md index 7889ef4971482..04f7ca2ed1eaf 100644 --- a/e2etests/web/regular_integration_tests/README.md +++ b/e2etests/web/regular_integration_tests/README.md @@ -5,12 +5,18 @@ configuration (e.g. PWA vs non-PWA packaging), please create another directory under e2etests/web. Otherwise tests such as text_editing, history, scrolling, pointer events... should all go under this package. -# To run the application under test for traouble shooting purposes. +Tests can be run on both 'release' and 'profile' modes. However 'release' mode +will shorten the error. Use 'profile' mode for trouble-shooting purposes where +you can also see the full stack trace. + +# To run the application under test for trouble shooting purposes. flutter run -d web-server lib/text_editing_main.dart --local-engine=host_debug_unopt # To run the Text Editing test and use the developer tools in the browser. -flutter run --target=test_driver/text_editing_e2e.dart -d web-server --web-port=8080 --release --local-engine=host_debug_unopt +flutter run --target=test_driver/text_editing_e2e.dart -d web-server --web-port=8080 --profile --local-engine=host_debug_unopt + +# To test the Text Editing test with driver you either of the following: +flutter drive -v --target=test_driver/text_editing_e2e.dart -d web-server --profile --browser-name=chrome --local-engine=host_debug_unopt -# To test the Text Editing test with driver: flutter drive -v --target=test_driver/text_editing_e2e.dart -d web-server --release --browser-name=chrome --local-engine=host_debug_unopt ``` diff --git a/e2etests/web/regular_integration_tests/assets/images/1.5x/sample_image1.png b/e2etests/web/regular_integration_tests/assets/images/1.5x/sample_image1.png new file mode 100644 index 0000000000000..f1e0ffab20007 Binary files /dev/null and b/e2etests/web/regular_integration_tests/assets/images/1.5x/sample_image1.png differ diff --git a/e2etests/web/regular_integration_tests/assets/images/2.0x/sample_image1.png b/e2etests/web/regular_integration_tests/assets/images/2.0x/sample_image1.png new file mode 100644 index 0000000000000..d80b0ff28c502 Binary files /dev/null and b/e2etests/web/regular_integration_tests/assets/images/2.0x/sample_image1.png differ diff --git a/e2etests/web/regular_integration_tests/assets/images/sample_image1.png b/e2etests/web/regular_integration_tests/assets/images/sample_image1.png new file mode 100644 index 0000000000000..0d1393b805213 Binary files /dev/null and b/e2etests/web/regular_integration_tests/assets/images/sample_image1.png differ diff --git a/e2etests/web/regular_integration_tests/lib/image_loading_main.dart b/e2etests/web/regular_integration_tests/lib/image_loading_main.dart new file mode 100644 index 0000000000000..78991d9652eb2 --- /dev/null +++ b/e2etests/web/regular_integration_tests/lib/image_loading_main.dart @@ -0,0 +1,32 @@ +// 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. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() async { + const MethodChannel channel = + OptionalMethodChannel('flutter/web_test_e2e', JSONMethodCodec()); + await channel.invokeMethod( + 'setDevicePixelRatio', + '1.5', + ); + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + @override + MyAppState createState() => MyAppState(); +} + +class MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + key: const Key('mainapp'), + title: 'Integration Test App', + home: Image.asset('assets/images/sample_image1.png') + ); + } +} diff --git a/e2etests/web/regular_integration_tests/lib/platform_messages_main.dart b/e2etests/web/regular_integration_tests/lib/platform_messages_main.dart new file mode 100644 index 0000000000000..4201da2b47b77 --- /dev/null +++ b/e2etests/web/regular_integration_tests/lib/platform_messages_main.dart @@ -0,0 +1,64 @@ +// 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. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() => runApp(MyApp()); + +Future dataFuture; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + key: const Key('mainapp'), + title: 'Integration Test App For Platform Messages', + home: MyHomePage(title: 'Integration Test App For Platform Messages'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final TextEditingController _controller = + TextEditingController(text: 'Text1'); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Hello World', + ), + // Create a text form field since we can't test clipboard unless + // html document has focus. + TextFormField( + key: const Key('input'), + enabled: true, + controller: _controller, + //initialValue: 'Text1', + decoration: const InputDecoration( + labelText: 'Text Input Field:', + ), + ), + ], + ), + ), + ); + } +} diff --git a/e2etests/web/regular_integration_tests/lib/treeshaking_main.dart b/e2etests/web/regular_integration_tests/lib/treeshaking_main.dart new file mode 100644 index 0000000000000..0df900d0597b4 --- /dev/null +++ b/e2etests/web/regular_integration_tests/lib/treeshaking_main.dart @@ -0,0 +1,45 @@ + +// 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. + +import 'package:flutter/material.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + key: const Key('mainapp'), + title: 'Integration Test App', + home: MyHomePage(title: 'Integration Test App'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final TextEditingController _controller = + TextEditingController(text: 'TreeshakingThings1'); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: const Center( + child: Text('TreeshakingThings'), + ), + ); + } +} diff --git a/e2etests/web/regular_integration_tests/pubspec.yaml b/e2etests/web/regular_integration_tests/pubspec.yaml index 98fa40773834e..3f0c3280b511b 100644 --- a/e2etests/web/regular_integration_tests/pubspec.yaml +++ b/e2etests/web/regular_integration_tests/pubspec.yaml @@ -13,6 +13,10 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - e2e: 0.2.4+4 + e2e: 0.4.0 http: 0.12.0+2 test: any + +flutter: + assets: + - assets/images/ diff --git a/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e.dart b/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e.dart new file mode 100644 index 0000000000000..3c858da3a9d08 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e.dart @@ -0,0 +1,24 @@ +// 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. + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:regular_integration_tests/image_loading_main.dart' as app; + +import 'package:e2e/e2e.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + testWidgets('Image loads asset variant based on device pixel ratio', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + final html.ImageElement imageElement = html.querySelector('img') as html.ImageElement; + expect(imageElement.naturalWidth, 1.5 * 100); + expect(imageElement.naturalHeight, 1.5 * 100); + expect(imageElement.width, 100); + expect(imageElement.height, 100); + }); +} diff --git a/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e_test.dart b/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e_test.dart new file mode 100644 index 0000000000000..a29203f7dcdd9 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/image_loading_e2e_test.dart @@ -0,0 +1,7 @@ +// 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. + +import 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e.dart b/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e.dart new file mode 100644 index 0000000000000..cd6a816b67f67 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e.dart @@ -0,0 +1,59 @@ +// 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. + +import 'dart:html' as html; +// ignore: undefined_shown_name +import 'dart:ui' as ui show platformViewRegistry; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:regular_integration_tests/platform_messages_main.dart' as app; + +import 'package:e2e/e2e.dart'; + +void main() async { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + testWidgets('platform message for Clipboard.setData reply with future', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + // TODO(nurhan): https://github.com/flutter/flutter/issues/51885 + SystemChannels.textInput.setMockMethodCallHandler(null); + // Focus on a TextFormField. + final Finder finder = find.byKey(const Key('input')); + expect(finder, findsOneWidget); + await tester.tap(find.byKey(const Key('input'))); + // Focus in input, otherwise clipboard will fail with + // 'document is not focused' platform exception. + html.document.querySelector('input').focus(); + await Clipboard.setData(const ClipboardData(text: 'sample text')); + }, skip: true); // https://github.com/flutter/flutter/issues/54296 + + testWidgets('Should create and dispose view embedder', + (WidgetTester tester) async { + int viewInstanceCount = 0; + + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory('MyView', (int viewId) { + ++viewInstanceCount; + return html.DivElement(); + }); + + app.main(); + await tester.pumpAndSettle(); + final Map createArgs = { + 'id': '567', + 'viewType': 'MyView', + }; + await SystemChannels.platform_views.invokeMethod('create', createArgs); + final Map disposeArgs = { + 'id': '567', + }; + await SystemChannels.platform_views.invokeMethod('dispose', disposeArgs); + expect(viewInstanceCount, 1); + }); +} diff --git a/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e_test.dart b/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e_test.dart new file mode 100644 index 0000000000000..a29203f7dcdd9 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/platform_messages_e2e_test.dart @@ -0,0 +1,7 @@ +// 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. + +import 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/e2etests/web/regular_integration_tests/test_driver/text_editing_e2e_test.dart b/e2etests/web/regular_integration_tests/test_driver/text_editing_e2e_test.dart index 26f4278d6505f..a29203f7dcdd9 100644 --- a/e2etests/web/regular_integration_tests/test_driver/text_editing_e2e_test.dart +++ b/e2etests/web/regular_integration_tests/test_driver/text_editing_e2e_test.dart @@ -2,18 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; +import 'package:e2e/e2e_driver.dart' as e2e; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - - // TODO(nurhan): https://github.com/flutter/flutter/issues/51940 - final String dataRequest = - await driver.requestData(null, timeout: const Duration(seconds: 1)); - print('result $dataRequest'); - await driver.close(); - - exit(dataRequest == 'pass' ? 0 : 1); -} +Future main() async => e2e.main(); diff --git a/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e.dart b/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e.dart new file mode 100644 index 0000000000000..30758c555f5b0 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e.dart @@ -0,0 +1,61 @@ +// 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. + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:regular_integration_tests/treeshaking_main.dart' as app; +import 'package:flutter/material.dart'; + +import 'package:e2e/e2e.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized() as E2EWidgetsFlutterBinding; + + testWidgets('debug+Fill+Properties for widgets is tree shaken', + (WidgetTester tester) async { + // About 11 instances are used by DiagnosticsNode and diagnostics + // for flutter framework itself. Widgets have > 100. So we check for 20 to + // so this test fails when tree-shaking is broken. + await testOccurenceCountBelow(tester, '${debugPrefix}FillProperties', 20); + }); +} + +// Used to prevent compiler optimization that will generate const string. +// Preventing counting test strings. +String get debugPrefix => ['d','e','b','u','g'].join(''); + +Future testOccurenceCountBelow(WidgetTester tester, String methodName, int count) async { + app.main(); + await tester.pumpAndSettle(); + + // Make sure app loaded. + final Finder finder = find.byKey(const Key('mainapp')); + expect(finder, findsOneWidget); + + await _loadBundleAndCheck(methodName, count); +} + +String fileContents; + +Future _loadBundleAndCheck(String methodName, int count) async { + fileContents ??= await html.HttpRequest.getString('main.dart.js'); + expect(fileContents, contains('RenderObjectToWidgetElement')); + expect(occurrenceCount(fileContents, methodName), lessThan(count)); +} + +int occurrenceCount(String contents, String word) { + int count = 0; + final int wordLength = word.length; + int pos = contents.indexOf(word); + final int contentLength = contents.length; + while (pos != -1) { + ++count; + pos += wordLength; + if (pos >= contentLength || count > 100) { + break; + } + pos = contents.indexOf(word, pos); + } + return count; +} diff --git a/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e_test.dart b/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e_test.dart new file mode 100644 index 0000000000000..a29203f7dcdd9 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/treeshaking_e2e_test.dart @@ -0,0 +1,7 @@ +// 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. + +import 'package:e2e/e2e_driver.dart' as e2e; + +Future main() async => e2e.main(); diff --git a/flow/BUILD.gn b/flow/BUILD.gn index 14ceec4dadc5c..c3fea8568e9ea 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -86,19 +86,11 @@ source_set("flow") { "view_holder.h", ] - if (using_fuchsia_sdk) { - public_deps += [ - "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", - "$fuchsia_sdk_root/pkg:scenic_cpp", - ] - } else { - public_deps += [ - "//garnet/public/lib/ui/scenic/cpp", - "//sdk/fidl/fuchsia.ui.scenic", - "//topaz/public/dart-pkg/zircon", - "//zircon/public/lib/zx", - ] - } + public_deps += [ + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.gfx", + "$fuchsia_sdk_root/pkg:scenic_cpp", + ] } } @@ -159,6 +151,10 @@ executable("flow_unittests") { "texture_unittests.cc", ] + if (is_fuchsia) { + sources += [ "layers/fuchsia_layer_unittests.cc" ] + } + deps = [ ":flow", ":flow_fixtures", @@ -170,6 +166,10 @@ executable("flow_unittests") { "//third_party/googletest:gtest", "//third_party/skia", ] + + if (is_fuchsia) { + deps += [ "//build/fuchsia/pkg:sys_cpp_testing" ] + } } if (is_fuchsia) { diff --git a/flow/README.md b/flow/README.md index a0a37a03d5840..150281e2288e4 100644 --- a/flow/README.md +++ b/flow/README.md @@ -3,4 +3,4 @@ Flow Flow is a simple compositor based on Skia that the Flutter engine uses to cache recoded paint commands and pixels generated from those recordings. Flow runs on -the GPU thread and uploads information to the GPU. +the raster thread and uploads information to Skia. diff --git a/flow/compositor_context.cc b/flow/compositor_context.cc index d816ca53ef877..e94ffd53f53d7 100644 --- a/flow/compositor_context.cc +++ b/flow/compositor_context.cc @@ -37,10 +37,10 @@ std::unique_ptr CompositorContext::AcquireFrame( const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger) { + fml::RefPtr raster_thread_merger) { return std::make_unique( *this, gr_context, canvas, view_embedder, root_surface_transformation, - instrumentation_enabled, surface_supports_readback, gpu_thread_merger); + instrumentation_enabled, surface_supports_readback, raster_thread_merger); } CompositorContext::ScopedFrame::ScopedFrame( @@ -51,7 +51,7 @@ CompositorContext::ScopedFrame::ScopedFrame( const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger) + fml::RefPtr raster_thread_merger) : context_(context), gr_context_(gr_context), canvas_(canvas), @@ -59,7 +59,7 @@ CompositorContext::ScopedFrame::ScopedFrame( root_surface_transformation_(root_surface_transformation), instrumentation_enabled_(instrumentation_enabled), surface_supports_readback_(surface_supports_readback), - gpu_thread_merger_(gpu_thread_merger) { + raster_thread_merger_(raster_thread_merger) { context_.BeginFrame(*this, instrumentation_enabled_); } @@ -74,8 +74,9 @@ RasterStatus CompositorContext::ScopedFrame::Raster( bool root_needs_readback = layer_tree.Preroll(*this, ignore_raster_cache); bool needs_save_layer = root_needs_readback && !surface_supports_readback(); PostPrerollResult post_preroll_result = PostPrerollResult::kSuccess; - if (view_embedder_ && gpu_thread_merger_) { - post_preroll_result = view_embedder_->PostPrerollAction(gpu_thread_merger_); + if (view_embedder_ && raster_thread_merger_) { + post_preroll_result = + view_embedder_->PostPrerollAction(raster_thread_merger_); } if (post_preroll_result == PostPrerollResult::kResubmitFrame) { diff --git a/flow/compositor_context.h b/flow/compositor_context.h index 6698d3f41a2f1..b932594be5d71 100644 --- a/flow/compositor_context.h +++ b/flow/compositor_context.h @@ -12,8 +12,8 @@ #include "flutter/flow/instrumentation.h" #include "flutter/flow/raster_cache.h" #include "flutter/flow/texture.h" -#include "flutter/fml/gpu_thread_merger.h" #include "flutter/fml/macros.h" +#include "flutter/fml/raster_thread_merger.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkPictureRecorder.h" @@ -46,7 +46,7 @@ class CompositorContext { const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger); + fml::RefPtr raster_thread_merger); virtual ~ScopedFrame(); @@ -75,7 +75,7 @@ class CompositorContext { const SkMatrix& root_surface_transformation_; const bool instrumentation_enabled_; const bool surface_supports_readback_; - fml::RefPtr gpu_thread_merger_; + fml::RefPtr raster_thread_merger_; FML_DISALLOW_COPY_AND_ASSIGN(ScopedFrame); }; @@ -91,7 +91,7 @@ class CompositorContext { const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger); + fml::RefPtr raster_thread_merger); void OnGrContextCreated(); diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index c660f4691318b..5234bf1e50c8c 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -6,10 +6,13 @@ namespace flutter { -bool ExternalViewEmbedder::SubmitFrame(GrContext* context) { +bool ExternalViewEmbedder::SubmitFrame(GrContext* context, + SkCanvas* background_canvas) { return false; }; +void ExternalViewEmbedder::FinishFrame(){}; + void MutatorsStack::PushClipRect(const SkRect& rect) { std::shared_ptr element = std::make_shared(rect); vector_.push_back(element); diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 030eb88c8a06d..14dd00cf5e8c8 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -7,8 +7,8 @@ #include -#include "flutter/fml/gpu_thread_merger.h" #include "flutter/fml/memory/ref_counted.h" +#include "flutter/fml/raster_thread_merger.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/core/SkPoint.h" @@ -239,7 +239,7 @@ class ExternalViewEmbedder { // after it does any requisite tasks needed to bring itself to a valid state. // Returns kSuccess if the view embedder is already in a valid state. virtual PostPrerollResult PostPrerollAction( - fml::RefPtr gpu_thread_merger) { + fml::RefPtr raster_thread_merger) { return PostPrerollResult::kSuccess; } @@ -248,7 +248,22 @@ class ExternalViewEmbedder { // Must be called on the UI thread. virtual SkCanvas* CompositeEmbeddedView(int view_id) = 0; - virtual bool SubmitFrame(GrContext* context); + virtual bool SubmitFrame(GrContext* context, SkCanvas* background_canvas); + + // This is called after submitting the embedder frame and the surface frame. + virtual void FinishFrame(); + + // This should only be called after |SubmitFrame|. + // This method provides the embedder a way to do additional tasks after + // |SubmitFrame|. After invoking this method, the current task on the + // TaskRunner should end immediately. + // + // For example on the iOS embedder, threads are merged in this call. + // A new frame on the platform thread starts immediately. If the GPU thread + // still has some task running, there could be two frames being rendered + // concurrently, which causes undefined behaviors. + virtual void EndFrame( + fml::RefPtr raster_thread_merger) {} FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); diff --git a/flow/layers/child_scene_layer.cc b/flow/layers/child_scene_layer.cc index 4a5358053928f..4e533130c1d4b 100644 --- a/flow/layers/child_scene_layer.cc +++ b/flow/layers/child_scene_layer.cc @@ -20,6 +20,7 @@ ChildSceneLayer::ChildSceneLayer(zx_koid_t layer_id, void ChildSceneLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ChildSceneLayer::Preroll"); set_needs_system_composite(true); + context->child_scene_layer_exists_below = true; // An alpha "hole punch" is required if the frame behind us is not opaque. if (!context->is_opaque) { @@ -49,7 +50,9 @@ void ChildSceneLayer::UpdateScene(SceneUpdateContext& context) { auto* view_holder = ViewHolder::FromId(layer_id_); FML_DCHECK(view_holder); - view_holder->UpdateScene(context, offset_, size_, hit_testable_); + view_holder->UpdateScene(context, offset_, size_, + SkScalarRoundToInt(context.alphaf() * 255), + hit_testable_); } } // namespace flutter diff --git a/flow/layers/fuchsia_layer_unittests.cc b/flow/layers/fuchsia_layer_unittests.cc new file mode 100644 index 0000000000000..ad9971d17e257 --- /dev/null +++ b/flow/layers/fuchsia_layer_unittests.cc @@ -0,0 +1,796 @@ +// 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 + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flutter/flow/layers/child_scene_layer.h" +#include "flutter/flow/layers/container_layer.h" +#include "flutter/flow/layers/opacity_layer.h" +#include "flutter/flow/layers/physical_shape_layer.h" +#include "flutter/flow/layers/transform_layer.h" +#include "flutter/flow/view_holder.h" +#include "flutter/fml/platform/fuchsia/message_loop_fuchsia.h" +#include "flutter/fml/task_runner.h" + +namespace flutter { +namespace testing { + +using FuchsiaLayerTest = ::testing::Test; + +class MockSession : public fuchsia::ui::scenic::testing::Session_TestBase { + public: + MockSession() : binding_(this) {} + + void NotImplemented_(const std::string& name) final {} + + void Bind(fidl::InterfaceRequest<::fuchsia::ui::scenic::Session> request, + ::fuchsia::ui::scenic::SessionListenerPtr listener) { + binding_.Bind(std::move(request)); + listener_ = std::move(listener); + } + + static std::string Vec3ValueToString(fuchsia::ui::gfx::Vector3Value value) { + return "{" + std::to_string(value.value.x) + ", " + + std::to_string(value.value.y) + ", " + + std::to_string(value.value.z) + "}"; + } + + static std::string QuaternionValueToString( + fuchsia::ui::gfx::QuaternionValue value) { + return "{" + std::to_string(value.value.x) + ", " + + std::to_string(value.value.y) + ", " + + std::to_string(value.value.z) + ", " + + std::to_string(value.value.w) + "}"; + } + + static std::string GfxCreateResourceCmdToString( + const fuchsia::ui::gfx::CreateResourceCmd& cmd) { + std::string id = " id: " + std::to_string(cmd.id); + switch (cmd.resource.Which()) { + case fuchsia::ui::gfx::ResourceArgs::Tag::kRectangle: + return "Rectangle" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kRoundedRectangle: + return "RoundedRectangle" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kViewHolder: + return "ViewHolder" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kOpacityNode: + return "OpacityNode" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kEntityNode: + return "EntityNode" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kShapeNode: + return "ShapeNode" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kMaterial: + return "Material" + id; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImage: + return "Image" + id + ", memory_id: " + + std::to_string(cmd.resource.image().memory_id) + + ", memory_offset: " + + std::to_string(cmd.resource.image().memory_offset); + default: + return "Unhandled CreateResource command" + + std::to_string(cmd.resource.Which()); + } + } + + static std::string GfxCmdToString(const fuchsia::ui::gfx::Command& cmd) { + switch (cmd.Which()) { + case fuchsia::ui::gfx::Command::Tag::kCreateResource: + return "CreateResource: " + + GfxCreateResourceCmdToString(cmd.create_resource()); + case fuchsia::ui::gfx::Command::Tag::kReleaseResource: + return "ReleaseResource id: " + + std::to_string(cmd.release_resource().id); + case fuchsia::ui::gfx::Command::Tag::kAddChild: + return "AddChild id: " + std::to_string(cmd.add_child().node_id) + + " child_id: " + std::to_string(cmd.add_child().child_id); + case fuchsia::ui::gfx::Command::Tag::kSetTranslation: + return "SetTranslation id: " + + std::to_string(cmd.set_translation().id) + + " value: " + Vec3ValueToString(cmd.set_translation().value); + case fuchsia::ui::gfx::Command::Tag::kSetScale: + return "SetScale id: " + std::to_string(cmd.set_scale().id) + + " value: " + Vec3ValueToString(cmd.set_scale().value); + case fuchsia::ui::gfx::Command::Tag::kSetRotation: + return "SetRotation id: " + std::to_string(cmd.set_rotation().id) + + " value: " + QuaternionValueToString(cmd.set_rotation().value); + case fuchsia::ui::gfx::Command::Tag::kSetOpacity: + return "SetOpacity id: " + std::to_string(cmd.set_opacity().node_id) + + ", opacity: " + std::to_string(cmd.set_opacity().opacity); + case fuchsia::ui::gfx::Command::Tag::kSetColor: + return "SetColor id: " + std::to_string(cmd.set_color().material_id) + + ", rgba: (" + std::to_string(cmd.set_color().color.value.red) + + ", " + std::to_string(cmd.set_color().color.value.green) + ", " + + std::to_string(cmd.set_color().color.value.blue) + ", " + + std::to_string(cmd.set_color().color.value.alpha) + ")"; + case fuchsia::ui::gfx::Command::Tag::kSetLabel: + return "SetLabel id: " + std::to_string(cmd.set_label().id) + " " + + cmd.set_label().label; + case fuchsia::ui::gfx::Command::Tag::kSetHitTestBehavior: + return "SetHitTestBehavior node_id: " + + std::to_string(cmd.set_hit_test_behavior().node_id); + case fuchsia::ui::gfx::Command::Tag::kSetClipPlanes: + return "SetClipPlanes node_id: " + + std::to_string(cmd.set_clip_planes().node_id); + case fuchsia::ui::gfx::Command::Tag::kSetShape: + return "SetShape node_id: " + std::to_string(cmd.set_shape().node_id) + + ", shape_id: " + std::to_string(cmd.set_shape().shape_id); + case fuchsia::ui::gfx::Command::Tag::kSetMaterial: + return "SetMaterial node_id: " + + std::to_string(cmd.set_material().node_id) + ", material_id: " + + std::to_string(cmd.set_material().material_id); + case fuchsia::ui::gfx::Command::Tag::kSetTexture: + return "SetTexture material_id: " + + std::to_string(cmd.set_texture().material_id) + + ", texture_id: " + std::to_string(cmd.set_texture().texture_id); + + default: + return "Unhandled gfx command" + std::to_string(cmd.Which()); + } + } + + static std::string ScenicCmdToString( + const fuchsia::ui::scenic::Command& cmd) { + if (cmd.Which() != fuchsia::ui::scenic::Command::Tag::kGfx) { + return "Unhandled non-gfx command: " + std::to_string(cmd.Which()); + } + return GfxCmdToString(cmd.gfx()); + } + + // |fuchsia::ui::scenic::Session| + void Enqueue(std::vector cmds) override { + for (const auto& cmd : cmds) { + num_enqueued_commands_++; + EXPECT_FALSE(expected_.empty()) + << "Received more commands than expected; command: <" + << ScenicCmdToString(cmd) + << ">, num_enqueued_commands: " << num_enqueued_commands_; + if (!expected_.empty()) { + EXPECT_TRUE(AreCommandsEqual(expected_.front(), cmd)) + << "actual command: <" << ScenicCmdToString(cmd) + << ">, expected command: <" << ScenicCmdToString(expected_.front()) + << ">, num_enqueued_commands: " << num_enqueued_commands_; + expected_.pop_front(); + } + } + } + + void SetExpectedCommands(std::vector gfx_cmds) { + std::deque scenic_commands; + for (auto it = gfx_cmds.begin(); it != gfx_cmds.end(); it++) { + scenic_commands.push_back(scenic::NewCommand(std::move((*it)))); + } + expected_ = std::move(scenic_commands); + num_enqueued_commands_ = 0; + } + + size_t num_enqueued_commands() { return num_enqueued_commands_; } + + private: + static bool IsGfxCommand(const fuchsia::ui::scenic::Command& cmd, + fuchsia::ui::gfx::Command::Tag tag) { + return cmd.Which() == fuchsia::ui::scenic::Command::Tag::kGfx && + cmd.gfx().Which() == tag; + } + + static bool IsCreateResourceCommand(const fuchsia::ui::scenic::Command& cmd, + fuchsia::ui::gfx::ResourceArgs::Tag tag) { + return IsGfxCommand(cmd, fuchsia::ui::gfx::Command::Tag::kCreateResource) && + cmd.gfx().create_resource().resource.Which() == tag; + } + + static bool AreCommandsEqual(const fuchsia::ui::scenic::Command& command1, + const fuchsia::ui::scenic::Command& command2) { + // For CreateViewHolderCommand, just compare the id and ignore the + // view_holder_token. + if (IsCreateResourceCommand( + command1, fuchsia::ui::gfx::ResourceArgs::Tag::kViewHolder)) { + return IsCreateResourceCommand( + command2, fuchsia::ui::gfx::ResourceArgs::Tag::kViewHolder) && + command1.gfx().create_resource().id == + command2.gfx().create_resource().id; + } + // For CreateImageCommand, just compare the id and memory_id. + if (IsCreateResourceCommand(command1, + fuchsia::ui::gfx::ResourceArgs::Tag::kImage)) { + return IsCreateResourceCommand( + command2, fuchsia::ui::gfx::ResourceArgs::Tag::kImage) && + command1.gfx().create_resource().id == + command2.gfx().create_resource().id && + command1.gfx().create_resource().resource.image().memory_id == + command2.gfx().create_resource().resource.image().memory_id; + } + // For SetHitTestBehaviorCommand, just compare the node_id. + if (IsGfxCommand(command1, + fuchsia::ui::gfx::Command::Tag::kSetHitTestBehavior)) { + return IsGfxCommand( + command2, + fuchsia::ui::gfx::Command::Tag::kSetHitTestBehavior) && + command1.gfx().set_hit_test_behavior().node_id == + command2.gfx().set_hit_test_behavior().node_id; + } + // For SetHitTestBehaviorCommand, just compare the node_id. + if (IsGfxCommand(command1, + fuchsia::ui::gfx::Command::Tag::kSetClipPlanes)) { + return IsGfxCommand(command2, + fuchsia::ui::gfx::Command::Tag::kSetClipPlanes) && + command1.gfx().set_clip_planes().node_id == + command2.gfx().set_clip_planes().node_id; + } + return fidl::Equals(command1, command2); + } + + std::deque expected_; + size_t num_enqueued_commands_ = 0; + fidl::Binding binding_; + fuchsia::ui::scenic::SessionListenerPtr listener_; +}; + +class MockSurfaceProducerSurface + : public SceneUpdateContext::SurfaceProducerSurface { + public: + MockSurfaceProducerSurface(scenic::Session* session, const SkISize& size) + : image_(session, 0, 0, {}), size_(size) {} + + size_t AdvanceAndGetAge() override { return 0; } + + bool FlushSessionAcquireAndReleaseEvents() override { return false; } + + bool IsValid() const override { return false; } + + SkISize GetSize() const override { return size_; } + + void SignalWritesFinished( + const std::function& on_writes_committed) override {} + + scenic::Image* GetImage() override { return &image_; }; + + sk_sp GetSkiaSurface() const override { return nullptr; }; + + private: + scenic::Image image_; + SkISize size_; +}; + +class MockSurfaceProducer : public SceneUpdateContext::SurfaceProducer { + public: + MockSurfaceProducer(scenic::Session* session) : session_(session) {} + std::unique_ptr ProduceSurface( + const SkISize& size, + const LayerRasterCacheKey& layer_key, + std::unique_ptr entity_node) override { + return std::make_unique(session_, size); + } + + // Query a retained entity node (owned by a retained surface) for retained + // rendering. + bool HasRetainedNode(const LayerRasterCacheKey& key) const override { + return false; + } + + scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) override { + return nullptr; + } + + void SubmitSurface(std::unique_ptr + surface) override {} + + private: + scenic::Session* session_; +}; + +struct TestContext { + // Message loop. + fml::RefPtr loop; + fml::RefPtr task_runner; + + // Session. + MockSession mock_session; + fidl::InterfaceRequest listener_request; + std::unique_ptr session; + + // SceneUpdateContext. + std::unique_ptr mock_surface_producer; + std::unique_ptr scene_update_context; + + // PrerollContext. + MutatorsStack unused_stack; + const Stopwatch unused_stopwatch; + TextureRegistry unused_texture_registry; + std::unique_ptr preroll_context; +}; + +std::unique_ptr InitTest() { + std::unique_ptr context = std::make_unique(); + + // Init message loop. + context->loop = fml::MakeRefCounted(); + context->task_runner = fml::MakeRefCounted(context->loop); + + // Init Session. + fuchsia::ui::scenic::SessionPtr session_ptr; + fuchsia::ui::scenic::SessionListenerPtr listener; + context->listener_request = listener.NewRequest(); + context->mock_session.Bind(session_ptr.NewRequest(), std::move(listener)); + context->session = std::make_unique(std::move(session_ptr)); + + // Init SceneUpdateContext. + context->mock_surface_producer = + std::make_unique(context->session.get()); + context->scene_update_context = std::make_unique( + context->session.get(), context->mock_surface_producer.get()); + context->scene_update_context->set_metrics( + fidl::MakeOptional(fuchsia::ui::gfx::Metrics{1.f, 1.f, 1.f})); + + // Init PrerollContext. + context->preroll_context = std::unique_ptr(new PrerollContext{ + nullptr, // raster_cache (don't consult the cache) + nullptr, // gr_context (used for the raster cache) + nullptr, // external view embedder + context->unused_stack, // mutator stack + nullptr, // SkColorSpace* dst_color_space + kGiantRect, // SkRect cull_rect + false, // layer reads from surface + context->unused_stopwatch, // frame time (dont care) + context->unused_stopwatch, // engine time (dont care) + context->unused_texture_registry, // texture registry (not + // supported) + false, // checkerboard_offscreen_layers + 100.f, // maximum depth allowed for rendering + 1.f // ratio between logical and physical + }); + + return context; +} + +zx_koid_t GetChildLayerId() { + static zx_koid_t sChildLayerId = 17324; + return sChildLayerId++; +} + +class AutoDestroyChildLayerId { + public: + AutoDestroyChildLayerId(zx_koid_t id) : id_(id) {} + ~AutoDestroyChildLayerId() { ViewHolder::Destroy(id_); } + + private: + zx_koid_t id_; +}; + +// Create a hierarchy with PhysicalShapeLayers and ChildSceneLayers, and +// inspect the commands sent to Scenic. +// +// +// What we expect: +// +// The Scenic elevations of the PhysicalShapeLayers are monotically +// increasing, even though the elevations we gave them when creating them are +// decreasing. The two should not have any correlation; we're merely mirror +// the paint order using Scenic elevation. +// +// PhysicalShapeLayers created before/below a ChildView do not get their own +// node; PhysicalShapeLayers created afterward do. +// +// Nested PhysicalShapeLayers are collapsed. +TEST_F(FuchsiaLayerTest, PhysicalShapeLayersAndChildSceneLayers) { + auto test_context = InitTest(); + + // Root. + auto root = std::make_shared(); + SkPath path; + path.addRect(SkRect::MakeWH(10.f, 10.f)); + + // Child #1: PhysicalShapeLayer. + auto physical_shape1 = std::make_shared( + /*color=*/SK_ColorCYAN, + /*shadow_color=*/SK_ColorBLACK, + /*elevation*/ 23.f, path, Clip::antiAlias); + root->Add(physical_shape1); + + // Child #2: ChildSceneLayer. + const zx_koid_t kChildLayerId1 = GetChildLayerId(); + auto [unused_view_token1, unused_view_holder_token1] = + scenic::ViewTokenPair::New(); + ViewHolder::Create(kChildLayerId1, test_context->task_runner, + std::move(unused_view_holder_token1), + /*bind_callback=*/[](scenic::ResourceId id) {}); + // Will destroy only when we go out of scope (i.e. end of the test). + AutoDestroyChildLayerId auto_destroy1(kChildLayerId1); + auto child_view1 = std::make_shared( + kChildLayerId1, SkPoint::Make(1, 1), SkSize::Make(10, 10), + /*hit_testable=*/false); + root->Add(child_view1); + + // Child #3: PhysicalShapeLayer + auto physical_shape2 = std::make_shared( + /*color=*/SK_ColorCYAN, + /*shadow_color=*/SK_ColorBLACK, + /*elevation*/ 21.f, path, Clip::antiAlias); + root->Add(physical_shape2); + + // Grandchild (child of #3): PhysicalShapeLayer + auto physical_shape3 = std::make_shared( + /*color=*/SK_ColorCYAN, + /*shadow_color=*/SK_ColorBLACK, + /*elevation*/ 19.f, path, Clip::antiAlias); + physical_shape2->Add(physical_shape3); + + // Child #4: ChildSceneLayer + const zx_koid_t kChildLayerId2 = GetChildLayerId(); + auto [unused_view_token2, unused_view_holder_token2] = + scenic::ViewTokenPair::New(); + ViewHolder::Create(kChildLayerId2, test_context->task_runner, + std::move(unused_view_holder_token2), + /*bind_callback=*/[](scenic::ResourceId id) {}); + // Will destroy only when we go out of scope (i.e. end of the test). + AutoDestroyChildLayerId auto_destroy2(kChildLayerId2); + auto child_view2 = std::make_shared( + kChildLayerId2, SkPoint::Make(1, 1), SkSize::Make(10, 10), + /*hit_testable=*/false); + root->Add(child_view2); + + // Child #5: PhysicalShapeLayer + auto physical_shape4 = std::make_shared( + /*color=*/SK_ColorCYAN, + /*shadow_color=*/SK_ColorBLACK, + /*elevation*/ 17.f, path, Clip::antiAlias); + root->Add(physical_shape4); + + // Preroll. + root->Preroll(test_context->preroll_context.get(), SkMatrix()); + + // Create another frame to be the "real" root. Required because + // UpdateScene() traversal expects there to already be a top node. + SceneUpdateContext::Frame frame(*(test_context->scene_update_context), + SkRRect::MakeRect(SkRect::MakeWH(100, 100)), + SK_ColorTRANSPARENT, SK_AlphaOPAQUE, + "fuchsia test root"); + + // Submit the list of command we will expect Scenic to see. + // + // Some things we expect: + // + // The Scenic elevations of the PhysicalShapeLayers are monotically + // increasing, even though the elevations we gave them when creating them are + // decreasing. The two should not have any correlation; we're merely mirror + // the paint order using Scenic elevation. + // + // PhysicalShapeLayers created before/below a ChildView do not get their own + // node; PhysicalShapeLayers created afterward do. + // + // Nested PhysicalShapeLayers are collapsed. + + std::vector expected; + + // + // Test root. + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/1)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/2)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/1, "fuchsia test root")); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/1, {0, 0})); + expected.push_back(scenic::NewAddChildCmd(/*id=*/1, /*child_id=*/2)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/2, kOneMinusEpsilon)); + + // + // Child #1: PhysicalShapeLayer + // + // Expect no new commands! Should be composited into base layer. + + // + // Child #2: ChildSceneLayer. + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/3)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/4)); + auto [view_token1, view_holder_token1] = scenic::ViewTokenPair::New(); + expected.push_back(scenic::NewCreateViewHolderCmd( + /*id=*/5, std::move(view_holder_token1), "")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/4, /*child_id=*/3)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/4, "flutter::ViewHolder")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/3, /*child_id=*/5)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/2, /*child_id=*/4)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/4, 1.f)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/3, {1, 1, -0.1})); + expected.push_back(scenic::NewSetHitTestBehaviorCmd( + /*id=*/3, /*ignored*/ fuchsia::ui::gfx::HitTestBehavior::kSuppress)); + + // + // Child #3: PhysicalShapeLayer + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/6)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/2, /*child_id=*/6)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/7)); + expected.push_back( + scenic::NewSetLabelCmd(/*id=*/6, "flutter::PhysicalShapeLayer")); + expected.push_back(scenic::NewSetTranslationCmd( + /*id=*/6, {0, 0, -kScenicZElevationBetweenLayers})); + expected.push_back(scenic::NewAddChildCmd(/*id=*/6, /*child_id=*/7)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/7, kOneMinusEpsilon)); + expected.push_back(scenic::NewSetClipPlanesCmd(/*id=*/6, /*ignored*/ {})); + expected.push_back(scenic::NewCreateShapeNodeCmd(/*id=*/8)); + expected.push_back(scenic::NewCreateRectangleCmd( + /*id=*/9, /*width=*/10, /*height=*/10)); + expected.push_back(scenic::NewSetShapeCmd(/*id=*/8, /*shape_id=*/9)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/8, {5, 5, 0})); + expected.push_back(scenic::NewCreateMaterialCmd(/*id=*/10)); + expected.push_back(scenic::NewSetMaterialCmd(/*id=*/8, /*material_id=*/10)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/6, /*child_id=*/8)); + + expected.push_back(scenic::NewCreateImageCmd(/*id=*/11, 0, 0, {})); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/6)); + expected.push_back(scenic::NewSetColorCmd(/*id=*/10, /*r*/ 255, /*g*/ 255, + /*b*/ 255, /*a*/ 255)); + expected.push_back( + scenic::NewSetTextureCmd(/*material_id=*/10, /*texture_id=*/11)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/10)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/9)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/8)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/7)); + + // + // Grandchild (child of #3): PhysicalShapeLayer + // + // Expect no new commands! Should be composited into parent. + + // + // Child #4: ChildSceneLayer + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/12)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/13)); + auto [view_token2, view_holder_token2] = scenic::ViewTokenPair::New(); + expected.push_back(scenic::NewCreateViewHolderCmd( + /*id=*/14, std::move(view_holder_token2), "")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/13, /*child_id=*/12)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/13, "flutter::ViewHolder")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/12, /*child_id=*/14)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/2, /*child_id=*/13)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/13, 1.f)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/12, {1, 1, -0.1})); + expected.push_back(scenic::NewSetHitTestBehaviorCmd( + /*id=*/12, /*ignored*/ fuchsia::ui::gfx::HitTestBehavior::kSuppress)); + + // + // Child #5: PhysicalShapeLayer + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/15)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/2, /*child_id=*/15)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/16)); + expected.push_back( + scenic::NewSetLabelCmd(/*id=*/15, "flutter::PhysicalShapeLayer")); + expected.push_back(scenic::NewSetTranslationCmd( + /*id=*/15, {0, 0, -2 * kScenicZElevationBetweenLayers})); + expected.push_back(scenic::NewAddChildCmd(/*id=*/15, /*child_id=*/16)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/16, kOneMinusEpsilon)); + expected.push_back(scenic::NewSetClipPlanesCmd(/*id=*/15, /*ignored*/ {})); + expected.push_back(scenic::NewCreateShapeNodeCmd(/*id=*/17)); + expected.push_back(scenic::NewCreateRectangleCmd( + /*id=*/18, /*width=*/10, /*height=*/10)); + expected.push_back(scenic::NewSetShapeCmd(/*id=*/17, /*shape_id=*/18)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/17, {5, 5, 0})); + expected.push_back(scenic::NewCreateMaterialCmd(/*id=*/19)); + expected.push_back(scenic::NewSetMaterialCmd(/*id=*/17, /*material_id=*/19)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/15, /*child_id=*/17)); + + expected.push_back(scenic::NewCreateImageCmd(/*id=*/20, 0, 0, {})); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/15)); + expected.push_back(scenic::NewSetColorCmd(/*id=*/19, /*r*/ 255, /*g*/ 255, + /*b*/ 255, /*a*/ 255)); + expected.push_back( + scenic::NewSetTextureCmd(/*material_id=*/19, /*texture_id=*/20)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/19)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/18)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/17)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/16)); + + test_context->mock_session.SetExpectedCommands(std::move(expected)); + + // Finally, UpdateScene(). The MockSession will check the emitted commands + // against the list above. + root->UpdateScene(*(test_context->scene_update_context)); + + test_context->session->Flush(); + + // Run loop until idle, so that the Session receives and processes + // its method calls. + async_loop_run_until_idle( + async_loop_from_dispatcher(async_get_default_dispatcher())); + + // Ensure we saw enough commands. + EXPECT_EQ(72u, test_context->mock_session.num_enqueued_commands()); +} + +// Create a hierarchy with OpacityLayers, TransformLayer, PhysicalShapeLayers +// and ChildSceneLayers, and inspect the commands sent to Scenic. +// +// We are interested in verifying that the opacity values of children are +// correct, and the transform values as well. +// +TEST_F(FuchsiaLayerTest, OpacityAndTransformLayer) { + auto test_context = InitTest(); + + // Root. + auto root = std::make_shared(); + SkPath path; + path.addRect(SkRect::MakeWH(10.f, 10.f)); + + // OpacityLayer #1 + auto opacity_layer1 = + std::make_shared(127, SkPoint::Make(0, 0)); + root->Add(opacity_layer1); + + // OpacityLayer #2 + auto opacity_layer2 = + std::make_shared(127, SkPoint::Make(0, 0)); + opacity_layer1->Add(opacity_layer2); + + // TransformLayer + SkMatrix translate_and_scale; + translate_and_scale.setScaleTranslate(1.1f, 1.1f, 2.f, 2.f); + auto transform_layer = std::make_shared(translate_and_scale); + opacity_layer2->Add(transform_layer); + + // TransformLayer Child #1: ChildSceneLayer. + const zx_koid_t kChildLayerId1 = GetChildLayerId(); + auto [unused_view_token1, unused_view_holder_token1] = + scenic::ViewTokenPair::New(); + + ViewHolder::Create(kChildLayerId1, test_context->task_runner, + std::move(unused_view_holder_token1), + /*bind_callback=*/[](scenic::ResourceId id) {}); + // Will destroy only when we go out of scope (i.e. end of the test). + AutoDestroyChildLayerId auto_destroy1(kChildLayerId1); + auto child_view1 = std::make_shared( + kChildLayerId1, SkPoint::Make(1, 1), SkSize::Make(10, 10), + /*hit_testable=*/false); + transform_layer->Add(child_view1); + + // TransformLayer Child #2: PhysicalShapeLayer. + auto physical_shape1 = std::make_shared( + /*color=*/SK_ColorCYAN, + /*shadow_color=*/SK_ColorBLACK, + /*elevation*/ 23.f, path, Clip::antiAlias); + transform_layer->Add(physical_shape1); + + // Preroll. + root->Preroll(test_context->preroll_context.get(), SkMatrix()); + + // Create another frame to be the "real" root. Required because + // UpdateScene() traversal expects there to already be a top node. + SceneUpdateContext::Frame frame(*(test_context->scene_update_context), + SkRRect::MakeRect(SkRect::MakeWH(100, 100)), + SK_ColorTRANSPARENT, SK_AlphaOPAQUE, + "fuchsia test root"); + + // Submit the list of command we will expect Scenic to see. + // + // We are interested in verifying that the opacity values of children are + // correct. + + std::vector expected; + + // + // Test root. + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/1)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/2)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/1, "fuchsia test root")); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/1, {0, 0, 0})); + expected.push_back(scenic::NewAddChildCmd(/*id=*/1, /*child_id=*/2)); + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/2, kOneMinusEpsilon)); + + // + // OpacityLayer #1 + // + // Expect no new commands for this. + + // + // OpacityLayer #2 + // + // Expect no new commands for this. + + // + // TransformLayer + // + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/3)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/2, /*child_id=*/3)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/3, "flutter::Transform")); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/3, {2.f, 2.f, 0.f})); + expected.push_back(scenic::NewSetScaleCmd(/*id=*/3, {1.1f, 1.1f, 1.f})); + expected.push_back(scenic::NewSetRotationCmd(/*id=*/3, {0.f, 0.f, 0.f, 1.f})); + + // + // TransformLayer Child #1: ChildSceneLayer. + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/4)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/5)); + auto [view_token1, view_holder_token1] = scenic::ViewTokenPair::New(); + expected.push_back(scenic::NewCreateViewHolderCmd( + /*id=*/6, std::move(view_holder_token1), "")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/5, /*child_id=*/4)); + expected.push_back(scenic::NewSetLabelCmd(/*id=*/5, "flutter::ViewHolder")); + expected.push_back(scenic::NewAddChildCmd(/*id=*/4, /*child_id=*/6)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/3, /*child_id=*/5)); + + // Check opacity value. Extra rounding required because we pass alpha as + // a uint/SkAlpha to SceneUpdateContext::Frame. + float opacity1 = kOneMinusEpsilon * (127 / 255.f) * (127 / 255.f); + opacity1 = SkScalarRoundToInt(opacity1 * 255) / 255.f; + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/5, opacity1)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/4, {1, 1, -0.1})); + expected.push_back(scenic::NewSetHitTestBehaviorCmd( + /*id=*/4, /*ignored*/ fuchsia::ui::gfx::HitTestBehavior::kSuppress)); + + // + // TransformLayer Child #2: PhysicalShapeLayer + // + expected.push_back(scenic::NewCreateEntityNodeCmd(/*id=*/7)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/3, /*child_id=*/7)); + expected.push_back(scenic::NewCreateOpacityNodeCmdHACK(/*id=*/8)); + expected.push_back( + scenic::NewSetLabelCmd(/*id=*/7, "flutter::PhysicalShapeLayer")); + expected.push_back(scenic::NewSetTranslationCmd( + /*id=*/7, {0, 0, -kScenicZElevationBetweenLayers})); + expected.push_back(scenic::NewAddChildCmd(/*id=*/7, /*child_id=*/8)); + + // Check opacity value. Extra rounding required because we pass alpha as + // a uint/SkAlpha to SceneUpdateContext::Frame. + float opacity2 = kOneMinusEpsilon * (127 / 255.f) * (127 / 255.f); + opacity2 = SkScalarRoundToInt(opacity2 * 255) / 255.f; + expected.push_back(scenic::NewSetOpacityCmd(/*id=*/8, opacity2)); + expected.push_back(scenic::NewSetClipPlanesCmd(/*id=*/7, /*ignored*/ {})); + expected.push_back(scenic::NewCreateShapeNodeCmd(/*id=*/9)); + expected.push_back(scenic::NewCreateRectangleCmd( + /*id=*/10, /*width=*/10, /*height=*/10)); + expected.push_back(scenic::NewSetShapeCmd(/*id=*/9, /*shape_id=*/10)); + expected.push_back(scenic::NewSetTranslationCmd(/*id=*/9, {5, 5, 0})); + expected.push_back(scenic::NewCreateMaterialCmd(/*id=*/11)); + expected.push_back(scenic::NewSetMaterialCmd(/*id=*/9, + /*material_id=*/11)); + expected.push_back(scenic::NewAddChildCmd(/*id=*/7, + /*child_id=*/9)); + + expected.push_back(scenic::NewCreateImageCmd(/*id=*/12, 0, 0, {})); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/7)); + expected.push_back(scenic::NewSetColorCmd(/*id=*/11, /*r*/ 255, + /*g*/ 255, + /*b*/ 255, /*a*/ 63)); + expected.push_back( + scenic::NewSetTextureCmd(/*material_id=*/11, /*texture_id=*/12)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/11)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/10)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/9)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/8)); + expected.push_back(scenic::NewReleaseResourceCmd(/*id=*/3)); + + test_context->mock_session.SetExpectedCommands(std::move(expected)); + + // Finally, UpdateScene(). The MockSession will check the emitted + // commands against the list above. + root->UpdateScene(*(test_context->scene_update_context)); + + test_context->session->Flush(); + + // Run loop until idle, so that the Session receives and processes + // its method calls. + async_loop_run_until_idle( + async_loop_from_dispatcher(async_get_default_dispatcher())); + + // Ensure we saw enough commands. + EXPECT_EQ(46u, test_context->mock_session.num_enqueued_commands()); +} + +} // namespace testing +} // namespace flutter diff --git a/flow/layers/layer.h b/flow/layers/layer.h index 50e2b0f7e68cf..dfd022a350a0f 100644 --- a/flow/layers/layer.h +++ b/flow/layers/layer.h @@ -66,6 +66,11 @@ struct PrerollContext { float total_elevation = 0.0f; bool has_platform_view = false; bool is_opaque = true; +#if defined(OS_FUCHSIA) + // True if, during the traversal so far, we have seen a child_scene_layer. + // Informs whether a layer needs to be system composited. + bool child_scene_layer_exists_below = false; +#endif // defined(OS_FUCHSIA) }; // Represents a single composited layer. Created on the UI thread but then diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc index 37c28e861bf8a..e7fc0ceadcc2f 100644 --- a/flow/layers/layer_tree.cc +++ b/flow/layers/layer_tree.cc @@ -21,8 +21,10 @@ LayerTree::LayerTree(const SkISize& frame_size, checkerboard_raster_cache_images_(false), checkerboard_offscreen_layers_(false) {} -void LayerTree::RecordBuildTime(fml::TimePoint start) { - build_start_ = start; +void LayerTree::RecordBuildTime(fml::TimePoint build_start, + fml::TimePoint target_time) { + build_start_ = build_start; + target_time_ = target_time; build_finish_ = fml::TimePoint::Now(); } @@ -83,7 +85,7 @@ void LayerTree::UpdateScene(SceneUpdateContext& context, context, SkRRect::MakeRect( SkRect::MakeWH(frame_size_.width(), frame_size_.height())), - SK_ColorTRANSPARENT, SK_AlphaOPAQUE); + SK_ColorTRANSPARENT, SK_AlphaOPAQUE, "flutter::LayerTree"); if (root_layer_->needs_system_composite()) { root_layer_->UpdateScene(context); } diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h index 43fc58a9746b6..39a88a30ae05c 100644 --- a/flow/layers/layer_tree.h +++ b/flow/layers/layer_tree.h @@ -54,10 +54,11 @@ class LayerTree { float frame_physical_depth() const { return frame_physical_depth_; } float frame_device_pixel_ratio() const { return frame_device_pixel_ratio_; } - void RecordBuildTime(fml::TimePoint begin_start); + void RecordBuildTime(fml::TimePoint build_start, fml::TimePoint target_time); fml::TimePoint build_start() const { return build_start_; } fml::TimePoint build_finish() const { return build_finish_; } fml::TimeDelta build_time() const { return build_finish_ - build_start_; } + fml::TimePoint target_time() const { return target_time_; } // The number of frame intervals missed after which the compositor must // trace the rasterized picture to a trace file. Specify 0 to disable all @@ -84,6 +85,7 @@ class LayerTree { std::shared_ptr root_layer_; fml::TimePoint build_start_; fml::TimePoint build_finish_; + fml::TimePoint target_time_; SkISize frame_size_ = SkISize::MakeEmpty(); // Physical pixels. float frame_physical_depth_; float frame_device_pixel_ratio_ = 1.0f; // Logical / Physical pixels ratio. diff --git a/flow/layers/opacity_layer.cc b/flow/layers/opacity_layer.cc index e6ca90a66a42f..7899c31784b89 100644 --- a/flow/layers/opacity_layer.cc +++ b/flow/layers/opacity_layer.cc @@ -9,11 +9,6 @@ namespace flutter { -// The OpacityLayer has no real "elevation", but we want to avoid Z-fighting -// when using the system compositor. Choose a small but non-zero value for -// this. -constexpr float kOpacityElevationWhenUsingSystemCompositor = 0.01f; - OpacityLayer::OpacityLayer(SkAlpha alpha, const SkPoint& offset) : alpha_(alpha), offset_(offset) { // Ensure OpacityLayer has only one direct child. @@ -40,8 +35,6 @@ void OpacityLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { SkMatrix child_matrix = matrix; child_matrix.postTranslate(offset_.fX, offset_.fY); - total_elevation_ = context->total_elevation; - context->total_elevation += kOpacityElevationWhenUsingSystemCompositor; context->is_opaque = parent_is_opaque && (alpha_ == SK_AlphaOPAQUE); context->mutators_stack.PushTransform( SkMatrix::MakeTrans(offset_.fX, offset_.fY)); @@ -52,17 +45,7 @@ void OpacityLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { context->mutators_stack.Pop(); context->mutators_stack.Pop(); context->is_opaque = parent_is_opaque; - context->total_elevation = total_elevation_; -#if defined(OS_FUCHSIA) - if (needs_system_composite()) { - // When using the system compositor, do not include the offset since we - // are rendering as a separate piece of geometry and the offset will be - // baked into that geometry's transform. - frameRRect_ = SkRRect::MakeRect(paint_bounds()); - set_paint_bounds(SkRect::MakeEmpty()); - } else -#endif { set_paint_bounds(paint_bounds().makeOffset(offset_.fX, offset_.fY)); if (!context->has_platform_view && context->raster_cache && @@ -123,36 +106,10 @@ void OpacityLayer::Paint(PaintContext& context) const { #if defined(OS_FUCHSIA) void OpacityLayer::UpdateScene(SceneUpdateContext& context) { - FML_DCHECK(needs_system_composite()); - TRACE_EVENT0("flutter", "OpacityLayer::UpdateScene"); - - ContainerLayer* container = GetChildContainer(); - FML_DCHECK(!container->layers().empty()); // OpacityLayer can't be a leaf. - - SceneUpdateContext::Transform transform( - context, SkMatrix::MakeTrans(offset_.fX, offset_.fY)); - - // Retained rendering: speedup by reusing a retained entity node if possible. - // When an entity node is reused, no paint layer is added to the frame so we - // won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - const scenic::EntityNode& retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node.session() == context.session()); - context.top_entity()->embedder_node().AddChild(retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame( - context, frameRRect_, SK_ColorTRANSPARENT, alpha_, - kOpacityElevationWhenUsingSystemCompositor, total_elevation_, this); - frame.AddPaintLayer(container); - - UpdateSceneChildren(context); + float saved_alpha = context.alphaf(); + context.set_alphaf(context.alphaf() * (alpha_ / 255.f)); + ContainerLayer::UpdateScene(context); + context.set_alphaf(saved_alpha); } #endif // defined(OS_FUCHSIA) diff --git a/flow/layers/opacity_layer.h b/flow/layers/opacity_layer.h index efc5dd4f83e04..658dd1a7b8488 100644 --- a/flow/layers/opacity_layer.h +++ b/flow/layers/opacity_layer.h @@ -43,7 +43,6 @@ class OpacityLayer : public ContainerLayer { SkAlpha alpha_; SkPoint offset_; SkRRect frameRRect_; - float total_elevation_ = 0.0f; FML_DISALLOW_COPY_AND_ASSIGN(OpacityLayer); }; diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc index ef7b6f2c6194c..dacb9428604f8 100644 --- a/flow/layers/performance_overlay_layer.cc +++ b/flow/layers/performance_overlay_layer.cc @@ -89,7 +89,7 @@ void PerformanceOverlayLayer::Paint(PaintContext& context) const { VisualizeStopWatch( *context.leaf_nodes_canvas, context.raster_time, x, y, width, height - padding, options_ & kVisualizeRasterizerStatistics, - options_ & kDisplayRasterizerStatistics, "GPU", font_path_); + options_ & kDisplayRasterizerStatistics, "Raster", font_path_); VisualizeStopWatch(*context.leaf_nodes_canvas, context.ui_time, x, y + height, width, height - padding, diff --git a/flow/layers/performance_overlay_layer_unittests.cc b/flow/layers/performance_overlay_layer_unittests.cc index 769f80803a8fa..3fcb2e04e835b 100644 --- a/flow/layers/performance_overlay_layer_unittests.cc +++ b/flow/layers/performance_overlay_layer_unittests.cc @@ -88,6 +88,8 @@ static void TestPerformanceOverlayLayerGold(int refresh_rate) { << "Please either set --golden-dir, or make sure that the unit test is " << "run from the right directory (e.g., flutter/engine/src)."; + // TODO(https://github.com/flutter/flutter/issues/53784): enable this on all + // platforms. #if !defined(OS_LINUX) GTEST_SKIP() << "Skipping golden tests on non-Linux OSes"; #endif // OS_LINUX @@ -163,7 +165,7 @@ TEST_F(PerformanceOverlayLayerTest, SimpleRasterizerStatistics) { layer->Paint(paint_context()); auto overlay_text = PerformanceOverlayLayer::MakeStatisticsText( - paint_context().raster_time, "GPU", ""); + paint_context().raster_time, "Raster", ""); auto overlay_text_data = overlay_text->serialize(SkSerialProcs{}); SkPaint text_paint; text_paint.setColor(SK_ColorGRAY); diff --git a/flow/layers/physical_shape_layer.cc b/flow/layers/physical_shape_layer.cc index c64049222a8e7..675ec5917c5df 100644 --- a/flow/layers/physical_shape_layer.cc +++ b/flow/layers/physical_shape_layer.cc @@ -52,23 +52,31 @@ void PhysicalShapeLayer::Preroll(PrerollContext* context, context->total_elevation += elevation_; total_elevation_ = context->total_elevation; +#if defined(OS_FUCHSIA) + child_layer_exists_below_ = context->child_scene_layer_exists_below; + context->child_scene_layer_exists_below = false; +#endif + SkRect child_paint_bounds; PrerollChildren(context, matrix, &child_paint_bounds); + +#if defined(OS_FUCHSIA) + if (child_layer_exists_below_) { + set_needs_system_composite(true); + } + context->child_scene_layer_exists_below = + context->child_scene_layer_exists_below || child_layer_exists_below_; +#endif context->total_elevation -= elevation_; if (elevation_ == 0) { set_paint_bounds(path_.getBounds()); } else { -#if defined(OS_FUCHSIA) - // Let the system compositor draw all shadows for us. - set_needs_system_composite(true); -#else // We will draw the shadow in Paint(), so add some margin to the paint // bounds to leave space for the shadow. We fill this whole region and clip // children to it so we don't need to join the child paint bounds. set_paint_bounds(ComputeShadowBounds(path_.getBounds(), elevation_, context->frame_device_pixel_ratio)); -#endif // defined(OS_FUCHSIA) } } @@ -78,30 +86,52 @@ void PhysicalShapeLayer::UpdateScene(SceneUpdateContext& context) { FML_DCHECK(needs_system_composite()); TRACE_EVENT0("flutter", "PhysicalShapeLayer::UpdateScene"); - // Retained rendering: speedup by reusing a retained entity node if possible. - // When an entity node is reused, no paint layer is added to the frame so we - // won't call PhysicalShapeLayer::Paint. - LayerRasterCacheKey key(unique_id(), context.Matrix()); - if (context.HasRetainedNode(key)) { - TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); - const scenic::EntityNode& retained_node = context.GetRetainedNode(key); - FML_DCHECK(context.top_entity()); - FML_DCHECK(retained_node.session() == context.session()); - context.top_entity()->entity_node().AddChild(retained_node); - return; - } - - TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); - // If we can't find an existing retained surface, create one. - SceneUpdateContext::Frame frame(context, frameRRect_, color_, SK_AlphaOPAQUE, - elevation_, total_elevation_, this); - for (auto& layer : layers()) { - if (layer->needs_painting()) { - frame.AddPaintLayer(layer.get()); + // If there is embedded Fuchsia content in the scene (a ChildSceneLayer), + // PhysicalShapeLayers that appear above the embedded content will be turned + // into their own Scenic layers. + if (child_layer_exists_below_) { + float global_scenic_elevation = + context.GetGlobalElevationForNextScenicLayer(); + float local_scenic_elevation = + global_scenic_elevation - context.scenic_elevation(); + float z_translation = -local_scenic_elevation; + + // Retained rendering: speedup by reusing a retained entity node if + // possible. When an entity node is reused, no paint layer is added to the + // frame so we won't call PhysicalShapeLayer::Paint. + LayerRasterCacheKey key(unique_id(), context.Matrix()); + if (context.HasRetainedNode(key)) { + TRACE_EVENT_INSTANT0("flutter", "retained layer cache hit"); + scenic::EntityNode* retained_node = context.GetRetainedNode(key); + FML_DCHECK(context.top_entity()); + FML_DCHECK(retained_node->session() == context.session()); + + // Re-adjust the elevation. + retained_node->SetTranslation(0.f, 0.f, z_translation); + + context.top_entity()->entity_node().AddChild(*retained_node); + return; } - } - UpdateSceneChildren(context); + TRACE_EVENT_INSTANT0("flutter", "cache miss, creating"); + // If we can't find an existing retained surface, create one. + SceneUpdateContext::Frame frame(context, frameRRect_, SK_ColorTRANSPARENT, + SkScalarRoundToInt(context.alphaf() * 255), + "flutter::PhysicalShapeLayer", + z_translation, this); + + frame.AddPaintLayer(this); + + // Node: UpdateSceneChildren needs to be called here so that |frame| is + // still in scope (and therefore alive) while UpdateSceneChildren is being + // called. + float scenic_elevation = context.scenic_elevation(); + context.set_scenic_elevation(scenic_elevation + local_scenic_elevation); + ContainerLayer::UpdateSceneChildren(context); + context.set_scenic_elevation(scenic_elevation); + } else { + ContainerLayer::UpdateSceneChildren(context); + } } #endif // defined(OS_FUCHSIA) diff --git a/flow/layers/physical_shape_layer.h b/flow/layers/physical_shape_layer.h index 40ff19cfac348..106327f47ec9c 100644 --- a/flow/layers/physical_shape_layer.h +++ b/flow/layers/physical_shape_layer.h @@ -42,6 +42,9 @@ class PhysicalShapeLayer : public ContainerLayer { float total_elevation() const { return total_elevation_; } private: +#if defined(OS_FUCHSIA) + bool child_layer_exists_below_ = false; +#endif SkColor color_; SkColor shadow_color_; float elevation_ = 0.0f; diff --git a/flow/layers/physical_shape_layer_unittests.cc b/flow/layers/physical_shape_layer_unittests.cc index d13116c9c44a0..df247e394f1f9 100644 --- a/flow/layers/physical_shape_layer_unittests.cc +++ b/flow/layers/physical_shape_layer_unittests.cc @@ -126,17 +126,11 @@ TEST_F(PhysicalShapeLayerTest, ElevationSimple) { layer->Preroll(preroll_context(), SkMatrix()); // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and // their shadows , so we do not do any painting there. -#if defined(OS_FUCHSIA) - EXPECT_EQ(layer->paint_bounds(), kEmptyRect); - EXPECT_FALSE(layer->needs_painting()); - EXPECT_TRUE(layer->needs_system_composite()); -#else EXPECT_EQ(layer->paint_bounds(), PhysicalShapeLayer::ComputeShadowBounds(layer_path.getBounds(), initial_elevation, 1.0f)); EXPECT_TRUE(layer->needs_painting()); EXPECT_FALSE(layer->needs_system_composite()); -#endif EXPECT_EQ(layer->total_elevation(), initial_elevation); // The Fuchsia system compositor handles all elevated PhysicalShapeLayers and @@ -187,18 +181,12 @@ TEST_F(PhysicalShapeLayerTest, ElevationComplex) { // On Fuchsia, the system compositor handles all elevated // PhysicalShapeLayers and their shadows , so we do not do any painting // there. -#if defined(OS_FUCHSIA) - EXPECT_EQ(layers[i]->paint_bounds(), kEmptyRect); - EXPECT_FALSE(layers[i]->needs_painting()); - EXPECT_TRUE(layers[i]->needs_system_composite()); -#else EXPECT_EQ(layers[i]->paint_bounds(), (PhysicalShapeLayer::ComputeShadowBounds( layer_path.getBounds(), initial_elevations[i], 1.0f /* pixel_ratio */))); EXPECT_TRUE(layers[i]->needs_painting()); EXPECT_FALSE(layers[i]->needs_system_composite()); -#endif EXPECT_EQ(layers[i]->total_elevation(), total_elevations[i]); } diff --git a/flow/layers/picture_layer.cc b/flow/layers/picture_layer.cc index 3bc7e394c1033..08c09cc9e833b 100644 --- a/flow/layers/picture_layer.cc +++ b/flow/layers/picture_layer.cc @@ -59,7 +59,7 @@ void PictureLayer::Paint(PaintContext& context) const { return; } } - context.leaf_nodes_canvas->drawPicture(picture()); + picture()->playback(context.leaf_nodes_canvas); } } // namespace flutter diff --git a/flow/layers/picture_layer_unittests.cc b/flow/layers/picture_layer_unittests.cc index 687c870eeac66..4f565cf500ecc 100644 --- a/flow/layers/picture_layer_unittests.cc +++ b/flow/layers/picture_layer_unittests.cc @@ -94,9 +94,6 @@ TEST_F(PictureLayerTest, SimplePicture) { 1, MockCanvas::SetMatrixData{RasterCache::GetIntegralTransCTM( layer_offset_matrix)}}, #endif - MockCanvas::DrawCall{ - 1, MockCanvas::DrawPictureData{mock_picture->serialize(), SkPaint(), - SkMatrix()}}, MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}}}); EXPECT_EQ(mock_canvas().draw_calls(), expected_draw_calls); } diff --git a/flow/layers/transform_layer.cc b/flow/layers/transform_layer.cc index 5c7bc44073236..dea9b8f18ebd3 100644 --- a/flow/layers/transform_layer.cc +++ b/flow/layers/transform_layer.cc @@ -56,8 +56,12 @@ void TransformLayer::UpdateScene(SceneUpdateContext& context) { TRACE_EVENT0("flutter", "TransformLayer::UpdateScene"); FML_DCHECK(needs_system_composite()); - SceneUpdateContext::Transform transform(context, transform_); - UpdateSceneChildren(context); + if (!transform_.isIdentity()) { + SceneUpdateContext::Transform transform(context, transform_); + UpdateSceneChildren(context); + } else { + UpdateSceneChildren(context); + } } #endif // defined(OS_FUCHSIA) diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc index f179905ed8e53..e7709b14c7bf0 100644 --- a/flow/raster_cache.cc +++ b/flow/raster_cache.cc @@ -26,7 +26,9 @@ void RasterCacheResult::draw(SkCanvas& canvas, const SkPaint* paint) const { SkAutoCanvasRestore auto_restore(&canvas, true); SkIRect bounds = RasterCache::GetDeviceBounds(logical_rect_, canvas.getTotalMatrix()); - FML_DCHECK(bounds.size() == image_->dimensions()); + FML_DCHECK( + std::abs(bounds.size().width() - image_->dimensions().width()) <= 1 && + std::abs(bounds.size().height() - image_->dimensions().height()) <= 1); canvas.resetMatrix(); canvas.drawImage(image_, bounds.fLeft, bounds.fTop, paint); } diff --git a/flow/raster_cache_unittests.cc b/flow/raster_cache_unittests.cc index 03e3b3879c469..0c00a47087f40 100644 --- a/flow/raster_cache_unittests.cc +++ b/flow/raster_cache_unittests.cc @@ -122,5 +122,37 @@ TEST(RasterCache, SweepsRemoveUnusedFrames) { ASSERT_FALSE(cache.Get(*picture, matrix).is_valid()); } +// Construct a cache result whose device target rectangle rounds out to be one +// pixel wider than the cached image. Verify that it can be drawn without +// triggering any assertions. +TEST(RasterCache, DeviceRectRoundOut) { + size_t threshold = 1; + flutter::RasterCache cache(threshold); + + SkPictureRecorder recorder; + SkRect logical_rect = SkRect::MakeLTRB(28, 0, 354.56731, 310.288); + recorder.beginRecording(logical_rect); + SkPaint paint; + paint.setColor(SK_ColorRED); + recorder.getRecordingCanvas()->drawRect(logical_rect, paint); + sk_sp picture = recorder.finishRecordingAsPicture(); + + SkMatrix ctm = SkMatrix::MakeAll(1.3312, 0, 233, 0, 1.3312, 206, 0, 0, 1); + + sk_sp srgb = SkColorSpace::MakeSRGB(); + ASSERT_FALSE( + cache.Prepare(NULL, picture.get(), ctm, srgb.get(), true, false)); + ASSERT_FALSE(cache.Get(*picture, ctm).is_valid()); + cache.SweepAfterFrame(); + ASSERT_TRUE(cache.Prepare(NULL, picture.get(), ctm, srgb.get(), true, false)); + ASSERT_TRUE(cache.Get(*picture, ctm).is_valid()); + + SkCanvas canvas(100, 100, nullptr); + canvas.setMatrix(ctm); + canvas.translate(248, 0); + + cache.Get(*picture, ctm).draw(canvas); +} + } // namespace testing } // namespace flutter diff --git a/flow/scene_update_context.cc b/flow/scene_update_context.cc index 59524bc9c6e56..1dadfd7327f23 100644 --- a/flow/scene_update_context.cc +++ b/flow/scene_update_context.cc @@ -72,14 +72,9 @@ void SceneUpdateContext::CreateFrame(scenic::EntityNode entity_node, // and possibly for its texture. // TODO(SCN-137): Need to be able to express the radii as vectors. scenic::ShapeNode shape_node(session()); - scenic::RoundedRectangle shape( - session_, // session - rrect.width(), // width - rrect.height(), // height - rrect.radii(SkRRect::kUpperLeft_Corner).x(), // top_left_radius - rrect.radii(SkRRect::kUpperRight_Corner).x(), // top_right_radius - rrect.radii(SkRRect::kLowerRight_Corner).x(), // bottom_right_radius - rrect.radii(SkRRect::kLowerLeft_Corner).x() // bottom_left_radius + scenic::Rectangle shape(session_, // session + rrect.width(), // width + rrect.height() // height ); shape_node.SetShape(shape); shape_node.SetTranslation(shape_bounds.width() * 0.5f + shape_bounds.left(), @@ -222,6 +217,9 @@ SceneUpdateContext::ExecutePaintTasks(CompositorContext::ScopedFrame& frame) { surfaces_to_submit.emplace_back(std::move(task.surface)); } paint_tasks_.clear(); + alpha_ = 1.f; + topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; + scenic_elevation_ = 0.f; return surfaces_to_submit; } @@ -244,19 +242,22 @@ SceneUpdateContext::Transform::Transform(SceneUpdateContext& context, : Entity(context), previous_scale_x_(context.top_scale_x_), previous_scale_y_(context.top_scale_y_) { + entity_node().SetLabel("flutter::Transform"); if (!transform.isIdentity()) { // TODO(SCN-192): The perspective and shear components in the matrix // are not handled correctly. MatrixDecomposition decomposition(transform); if (decomposition.IsValid()) { + // Don't allow clients to control the z dimension; we control that + // instead to make sure layers appear in proper order. entity_node().SetTranslation(decomposition.translation().x(), // decomposition.translation().y(), // - -decomposition.translation().z() // + 0.f // ); entity_node().SetScale(decomposition.scale().x(), // decomposition.scale().y(), // - decomposition.scale().z() // + 1.f // ); context.top_scale_x_ *= decomposition.scale().x(); context.top_scale_y_ *= decomposition.scale().y(); @@ -277,6 +278,7 @@ SceneUpdateContext::Transform::Transform(SceneUpdateContext& context, : Entity(context), previous_scale_x_(context.top_scale_x_), previous_scale_y_(context.top_scale_y_) { + entity_node().SetLabel("flutter::Transform"); if (scale_x != 1.f || scale_y != 1.f || scale_z != 1.f) { entity_node().SetScale(scale_x, scale_y, scale_z); context.top_scale_x_ *= scale_x; @@ -293,8 +295,8 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, const SkRRect& rrect, SkColor color, SkAlpha opacity, - float local_elevation, - float world_elevation, + std::string label, + float z_translation, Layer* layer) : Entity(context), rrect_(rrect), @@ -303,23 +305,14 @@ SceneUpdateContext::Frame::Frame(SceneUpdateContext& context, opacity_node_(context.session()), paint_bounds_(SkRect::MakeEmpty()), layer_(layer) { - const float depth = context.frame_physical_depth(); - if (depth > -1 && world_elevation > depth) { - // TODO(mklim): Deal with bounds overflow more elegantly. We'd like to be - // able to have developers specify the behavior here to alternatives besides - // clamping, like normalization on some arbitrary curve. - - // Clamp the local z coordinate at our max bound. Take into account the - // parent z position here to fix clamping in cases where the child is - // overflowing because of its parents. - const float parent_elevation = world_elevation - local_elevation; - local_elevation = depth - parent_elevation; - } - if (local_elevation != 0.0) { - entity_node().SetTranslation(0.f, 0.f, -local_elevation); - } + entity_node().SetLabel(label); + entity_node().SetTranslation(0.f, 0.f, z_translation); entity_node().AddChild(opacity_node_); - opacity_node_.SetOpacity(opacity_ / 255.0f); + // Scenic currently lacks an API to enable rendering of alpha channel; alpha + // channels are only rendered if there is a OpacityNode higher in the tree + // with opacity != 1. For now, clamp to a infinitesimally smaller value than + // 1, which does not cause visual problems in practice. + opacity_node_.SetOpacity(std::min(kOneMinusEpsilon, opacity_ / 255.0f)); } SceneUpdateContext::Frame::~Frame() { @@ -348,6 +341,7 @@ void SceneUpdateContext::Frame::AddPaintLayer(Layer* layer) { SceneUpdateContext::Clip::Clip(SceneUpdateContext& context, const SkRect& shape_bounds) : Entity(context) { + entity_node().SetLabel("flutter::Clip"); SetEntityNodeClipPlanes(entity_node(), shape_bounds); } diff --git a/flow/scene_update_context.h b/flow/scene_update_context.h index 77b0925cc6503..a4d1156cc6021 100644 --- a/flow/scene_update_context.h +++ b/flow/scene_update_context.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ #define FLUTTER_FLOW_SCENE_UPDATE_CONTEXT_H_ +#include #include #include #include @@ -22,6 +23,15 @@ namespace flutter { class Layer; +// Scenic currently lacks an API to enable rendering of alpha channel; this only +// happens if there is a OpacityNode higher in the tree with opacity != 1. For +// now, clamp to a infinitesimally smaller value than 1, which does not cause +// visual problems in practice. +constexpr float kOneMinusEpsilon = 1 - FLT_EPSILON; + +// How much layers are separated in Scenic z elevation. +constexpr float kScenicZElevationBetweenLayers = 10.f; + class SceneUpdateContext { public: class SurfaceProducerSurface { @@ -59,7 +69,7 @@ class SceneUpdateContext { // Query a retained entity node (owned by a retained surface) for retained // rendering. virtual bool HasRetainedNode(const LayerRasterCacheKey& key) const = 0; - virtual const scenic::EntityNode& GetRetainedNode( + virtual scenic::EntityNode* GetRetainedNode( const LayerRasterCacheKey& key) = 0; virtual void SubmitSurface( @@ -105,8 +115,8 @@ class SceneUpdateContext { const SkRRect& rrect, SkColor color, SkAlpha opacity, - float local_elevation = 0.0f, - float parent_elevation = 0.0f, + std::string label, + float z_translation = 0.0f, Layer* layer = nullptr); virtual ~Frame(); @@ -175,10 +185,25 @@ class SceneUpdateContext { bool HasRetainedNode(const LayerRasterCacheKey& key) const { return surface_producer_->HasRetainedNode(key); } - const scenic::EntityNode& GetRetainedNode(const LayerRasterCacheKey& key) { + scenic::EntityNode* GetRetainedNode(const LayerRasterCacheKey& key) { return surface_producer_->GetRetainedNode(key); } + // The cumulative alpha value based on all the parent OpacityLayers. + void set_alphaf(float alpha) { alpha_ = alpha; } + float alphaf() { return alpha_; } + + // The global scenic elevation at a given point in the traversal. + float scenic_elevation() { return scenic_elevation_; } + + void set_scenic_elevation(float elevation) { scenic_elevation_ = elevation; } + + float GetGlobalElevationForNextScenicLayer() { + float elevation = topmost_global_scenic_elevation_; + topmost_global_scenic_elevation_ += kScenicZElevationBetweenLayers; + return elevation; + } + private: struct PaintTask { std::unique_ptr surface; @@ -236,6 +261,10 @@ class SceneUpdateContext { float frame_device_pixel_ratio_ = 1.0f; // Ratio between logical and physical pixels. + float alpha_ = 1.0f; + float scenic_elevation_ = 0.f; + float topmost_global_scenic_elevation_ = kScenicZElevationBetweenLayers; + std::vector paint_tasks_; FML_DISALLOW_COPY_AND_ASSIGN(SceneUpdateContext); diff --git a/flow/testing/mock_texture.h b/flow/testing/mock_texture.h index c5339ebb77a66..5de0346f21629 100644 --- a/flow/testing/mock_texture.h +++ b/flow/testing/mock_texture.h @@ -25,7 +25,7 @@ class MockTexture : public Texture { explicit MockTexture(int64_t textureId); - // Called from GPU thread. + // Called from raster thread. void Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, diff --git a/flow/texture.cc b/flow/texture.cc index 15c93d360366e..c81314bf017fe 100644 --- a/flow/texture.cc +++ b/flow/texture.cc @@ -13,12 +13,19 @@ Texture::~Texture() = default; TextureRegistry::TextureRegistry() = default; void TextureRegistry::RegisterTexture(std::shared_ptr texture) { + if (!texture) { + return; + } mapping_[texture->Id()] = texture; } void TextureRegistry::UnregisterTexture(int64_t id) { - mapping_[id]->OnTextureUnregistered(); - mapping_.erase(id); + auto found = mapping_.find(id); + if (found == mapping_.end()) { + return; + } + found->second->OnTextureUnregistered(); + mapping_.erase(found); } void TextureRegistry::OnGrContextCreated() { diff --git a/flow/texture.h b/flow/texture.h index 812588d382bb1..ad4734798716c 100644 --- a/flow/texture.h +++ b/flow/texture.h @@ -15,25 +15,25 @@ namespace flutter { class Texture { public: - Texture(int64_t id); // Called from UI or GPU thread. - virtual ~Texture(); // Called from GPU thread. + Texture(int64_t id); // Called from UI or raster thread. + virtual ~Texture(); // Called from raster thread. - // Called from GPU thread. + // Called from raster thread. virtual void Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrContext* context) = 0; - // Called from GPU thread. + // Called from raster thread. virtual void OnGrContextCreated() = 0; - // Called from GPU thread. + // Called from raster thread. virtual void OnGrContextDestroyed() = 0; - // Called on GPU thread. + // Called on raster thread. virtual void MarkNewFrameAvailable() = 0; - // Called on GPU thread. + // Called on raster thread. virtual void OnTextureUnregistered() = 0; int64_t Id() { return id_; } @@ -48,19 +48,19 @@ class TextureRegistry { public: TextureRegistry(); - // Called from GPU thread. + // Called from raster thread. void RegisterTexture(std::shared_ptr texture); - // Called from GPU thread. + // Called from raster thread. void UnregisterTexture(int64_t id); - // Called from GPU thread. + // Called from raster thread. std::shared_ptr GetTexture(int64_t id); - // Called from GPU thread. + // Called from raster thread. void OnGrContextCreated(); - // Called from GPU thread. + // Called from raster thread. void OnGrContextDestroyed(); private: diff --git a/flow/view_holder.cc b/flow/view_holder.cc index 7f8929d933705..499d7b9ca05bb 100644 --- a/flow/view_holder.cc +++ b/flow/view_holder.cc @@ -52,8 +52,8 @@ void ViewHolder::Create(zx_koid_t id, fml::RefPtr ui_task_runner, fuchsia::ui::views::ViewHolderToken view_holder_token, const BindCallback& on_bind_callback) { - // This GPU thread contains at least 1 ViewHolder. Initialize the per-thread - // bindings. + // This raster thread contains at least 1 ViewHolder. Initialize the + // per-thread bindings. if (tls_view_holder_bindings.get() == nullptr) { tls_view_holder_bindings.reset(new ViewHolderBindings()); } @@ -102,13 +102,17 @@ ViewHolder::ViewHolder(fml::RefPtr ui_task_runner, void ViewHolder::UpdateScene(SceneUpdateContext& context, const SkPoint& offset, const SkSize& size, + SkAlpha opacity, bool hit_testable) { if (pending_view_holder_token_.value) { entity_node_ = std::make_unique(context.session()); + opacity_node_ = + std::make_unique(context.session()); view_holder_ = std::make_unique( context.session(), std::move(pending_view_holder_token_), "Flutter SceneHost"); - + opacity_node_->AddChild(*entity_node_); + opacity_node_->SetLabel("flutter::ViewHolder"); entity_node_->Attach(*view_holder_); ui_task_runner_->PostTask( [bind_callback = std::move(pending_bind_callback_), @@ -117,9 +121,11 @@ void ViewHolder::UpdateScene(SceneUpdateContext& context, }); } FML_DCHECK(entity_node_); + FML_DCHECK(opacity_node_); FML_DCHECK(view_holder_); - context.top_entity()->embedder_node().AddChild(*entity_node_); + context.top_entity()->embedder_node().AddChild(*opacity_node_); + opacity_node_->SetOpacity(opacity / 255.0f); entity_node_->SetTranslation(offset.x(), offset.y(), -0.1f); entity_node_->SetHitTestBehavior( hit_testable ? fuchsia::ui::gfx::HitTestBehavior::kDefault diff --git a/flow/view_holder.h b/flow/view_holder.h index 82d43eba826d4..10677b8157680 100644 --- a/flow/view_holder.h +++ b/flow/view_holder.h @@ -57,12 +57,14 @@ class ViewHolder { void UpdateScene(SceneUpdateContext& context, const SkPoint& offset, const SkSize& size, + SkAlpha opacity, bool hit_testable); private: fml::RefPtr ui_task_runner_; std::unique_ptr entity_node_; + std::unique_ptr opacity_node_; std::unique_ptr view_holder_; fuchsia::ui::views::ViewHolderToken pending_view_holder_token_; diff --git a/flutter_frontend_server/lib/server.dart b/flutter_frontend_server/lib/server.dart index db095bcd05776..85584d20329aa 100644 --- a/flutter_frontend_server/lib/server.dart +++ b/flutter_frontend_server/lib/server.dart @@ -8,9 +8,6 @@ import 'dart:async'; import 'dart:io' hide FileSystemEntity; import 'package:args/args.dart'; -import 'package:path/path.dart' as path; - -import 'package:vm/incremental_compiler.dart'; import 'package:frontend_server/frontend_server.dart' as frontend show FrontendCompiler, @@ -19,6 +16,9 @@ import 'package:frontend_server/frontend_server.dart' as frontend argParser, usage, ProgramTransformer; +import 'package:kernel/ast.dart'; +import 'package:path/path.dart' as path; +import 'package:vm/incremental_compiler.dart'; /// Wrapper around [FrontendCompiler] that adds [widgetCreatorTracker] kernel /// transformation to the compilation. @@ -72,7 +72,7 @@ class _FlutterFrontendCompiler implements frontend.CompilerInterface { expression, definitions, typeDefinitions, libraryUri, klass, isStatic); } - // ignore: annotate_overrides + @override Future compileExpressionToJs( String libraryUri, int line, @@ -81,7 +81,8 @@ class _FlutterFrontendCompiler implements frontend.CompilerInterface { Map jsFrameValues, String moduleName, String expression) { - throw UnimplementedError('Compile expression to JS is not supported'); + return _compiler.compileExpressionToJs(libraryUri, line, column, jsModules, + jsFrameValues, moduleName, expression); } @override @@ -107,6 +108,13 @@ Future starter( frontend.ProgramTransformer transformer, }) async { ArgResults options; + frontend.argParser.addMultiOption( + 'delete-tostring-package-uri', + help: 'Replaces implementations of `toString` with `super.toString()` for ' + 'specified package', + valueHelp: 'dart:ui', + defaultsTo: const [], + ); try { options = frontend.argParser.parse(args); } catch (error) { @@ -115,6 +123,8 @@ Future starter( return 1; } + final Set deleteToStringPackageUris = (options['delete-tostring-package-uri'] as List).toSet(); + if (options['train'] as bool) { if (!options.rest.isNotEmpty) { throw Exception('Must specify input.dart'); @@ -137,7 +147,10 @@ Future starter( '--gen-bytecode', '--bytecode-options=source-positions,local-var-info,debugger-stops,instance-field-initializers,keep-unreachable-code,avoid-closure-call-instructions', ]); - compiler ??= _FlutterFrontendCompiler(output); + compiler ??= _FlutterFrontendCompiler( + output, + transformer: ToStringTransformer(null, deleteToStringPackageUris), + ); await compiler.compile(input, options); compiler.acceptLastDelta(); @@ -156,7 +169,7 @@ Future starter( } compiler ??= _FlutterFrontendCompiler(output, - transformer: transformer, + transformer: ToStringTransformer(transformer, deleteToStringPackageUris), useDebuggerModuleNames: options['debugger-module-names'] as bool, unsafePackageSerialization: options['unsafe-package-serialization'] as bool); @@ -169,3 +182,86 @@ Future starter( frontend.listenAndCompile(compiler, input ?? stdin, options, completer); return completer.future; } + +// Transformer/visitor for toString +// If we add any more of these, they really should go into a separate library. + +/// A [RecursiveVisitor] that replaces [Object.toString] overrides with +/// `super.toString()`. +class ToStringVisitor extends RecursiveVisitor { + /// The [packageUris] must not be null. + ToStringVisitor(this._packageUris) : assert(_packageUris != null); + + /// A set of package URIs to apply this transformer to, e.g. 'dart:ui' and + /// 'package:flutter/foundation.dart'. + final Set _packageUris; + + /// Turn 'dart:ui' into 'dart:ui', or + /// 'package:flutter/src/semantics_event.dart' into 'package:flutter'. + String _importUriToPackage(Uri importUri) => '${importUri.scheme}:${importUri.pathSegments.first}'; + + bool _isInTargetPackage(Procedure node) { + return _packageUris.contains(_importUriToPackage(node.enclosingLibrary.importUri)); + } + + bool _hasKeepAnnotation(Procedure node) { + for (ConstantExpression expression in node.annotations.whereType()) { + if (expression.constant is! InstanceConstant) { + continue; + } + final InstanceConstant constant = expression.constant as InstanceConstant; + if (constant.classNode.name == '_KeepToString' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:ui') { + return true; + } + } + return false; + } + + @override + void visitProcedure(Procedure node) { + if ( + node.name.name == 'toString' && + node.enclosingClass != null && + node.enclosingLibrary != null && + !node.isStatic && + !node.isAbstract && + !node.enclosingClass.isEnum && + _isInTargetPackage(node) && + !_hasKeepAnnotation(node) + ) { + node.function.body.replaceWith( + ReturnStatement( + SuperMethodInvocation( + node.name, + Arguments([]), + ), + ), + ); + } + } + + @override + void defaultMember(Member node) {} +} + +/// Replaces [Object.toString] overrides with calls to super for the specified +/// [packageUris]. +class ToStringTransformer extends frontend.ProgramTransformer { + /// The [packageUris] parameter must not be null, but may be empty. + ToStringTransformer(this._child, this._packageUris) : assert(_packageUris != null); + + final frontend.ProgramTransformer _child; + + /// A set of package URIs to apply this transformer to, e.g. 'dart:ui' and + /// 'package:flutter/foundation.dart'. + final Set _packageUris; + + @override + void transform(Component component) { + assert(_child is! ToStringTransformer); + if (_packageUris.isNotEmpty) { + component.visitChildren(ToStringVisitor(_packageUris)); + } + _child?.transform(component); + } +} diff --git a/flutter_frontend_server/test/fixtures/.gitignore b/flutter_frontend_server/test/fixtures/.gitignore new file mode 100644 index 0000000000000..160ff8892218b --- /dev/null +++ b/flutter_frontend_server/test/fixtures/.gitignore @@ -0,0 +1 @@ +!.packages diff --git a/flutter_frontend_server/test/fixtures/.packages b/flutter_frontend_server/test/fixtures/.packages new file mode 100644 index 0000000000000..6164a98c24f5a --- /dev/null +++ b/flutter_frontend_server/test/fixtures/.packages @@ -0,0 +1,2 @@ +# Generated by pub on 2020-01-15 10:08:29.776333. +flutter_frontend_fixtures:lib/ diff --git a/flutter_frontend_server/test/fixtures/lib/main.dart b/flutter_frontend_server/test/fixtures/lib/main.dart new file mode 100644 index 0000000000000..fdd3bb9045c79 --- /dev/null +++ b/flutter_frontend_server/test/fixtures/lib/main.dart @@ -0,0 +1,27 @@ +// 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. + +import 'dart:convert'; +import 'dart:ui'; + +void main() { + final Paint paint = Paint()..color = Color(0xFFFFFFFF); + print(jsonEncode({ + 'Paint.toString': paint.toString(), + 'Brightness.toString': Brightness.dark.toString(), + 'Foo.toString': Foo().toString(), + 'Keep.toString': Keep().toString(), + })); +} + +class Foo { + @override + String toString() => 'I am a Foo'; +} + +class Keep { + @keepToString + @override + String toString() => 'I am a Keep'; +} diff --git a/flutter_frontend_server/test/to_string_test.dart b/flutter_frontend_server/test/to_string_test.dart new file mode 100644 index 0000000000000..406d595716a71 --- /dev/null +++ b/flutter_frontend_server/test/to_string_test.dart @@ -0,0 +1,314 @@ +// 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. + +import 'dart:io'; + +import 'package:flutter_frontend_server/server.dart'; +import 'package:frontend_server/frontend_server.dart' as frontend show ProgramTransformer; +import 'package:kernel/kernel.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as path; + +import 'package:test/test.dart'; + +void main(List args) async { + if (args.length != 2) { + stderr.writeln('The first argument must be the path to the forntend server dill.'); + stderr.writeln('The second argument must be the path to the flutter_patched_sdk'); + exit(-1); + } + + const Set uiAndFlutter = { + 'dart:ui', + 'package:flutter', + }; + + test('No packages', () { + final ToStringTransformer transformer = ToStringTransformer(null, {}); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verifyNever(component.visitChildren(any)); + }); + + test('dart:ui package', () { + final ToStringTransformer transformer = ToStringTransformer(null, uiAndFlutter); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verify(component.visitChildren(any)).called(1); + }); + + test('Child transformer', () { + final MockTransformer childTransformer = MockTransformer(); + final ToStringTransformer transformer = ToStringTransformer(childTransformer, {}); + + final MockComponent component = MockComponent(); + transformer.transform(component); + verifyNever(component.visitChildren(any)); + verify(childTransformer.transform(component)).called(1); + }); + + test('ToStringVisitor ignores non-toString procedures', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + when(procedure.name).thenReturn(Name('main')); + when(procedure.annotations).thenReturn(const []); + + visitor.visitProcedure(procedure); + verifyNever(procedure.enclosingLibrary); + }); + + test('ToStringVisitor ignores top level toString', () { + // i.e. a `toString` method specified at the top of a library, like: + // + // void main() {} + // String toString() => 'why?'; + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(null); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores abstract toString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(true); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores static toString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(true); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores enum toString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()..isEnum = true); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores non-specified libraries', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:some_package/src/blah.dart')); + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(Name('toString')); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + test('ToStringVisitor ignores @keepToString', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('dart:ui')); + final Name name = Name('toString'); + final Class annotation = Class(name: '_KeepToString')..parent = Library(Uri.parse('dart:ui')); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn([ + ConstantExpression( + InstanceConstant( + Reference()..node = annotation, + [], + {}, + ), + ), + ]); + + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + verifyNever(statement.replaceWith(any)); + }); + + void _validateReplacement(MockStatement body) { + final ReturnStatement replacement = verify(body.replaceWith(captureAny)).captured.single as ReturnStatement; + expect(replacement.expression, isA()); + final SuperMethodInvocation superMethodInvocation = replacement.expression as SuperMethodInvocation; + expect(superMethodInvocation.name.name, 'toString'); + } + + test('ToStringVisitor replaces toString in specified libraries (dart:ui)', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('dart:ui')); + final Name name = Name('toString'); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + _validateReplacement(statement); + }); + + test('ToStringVisitor replaces toString in specified libraries (package:flutter)', () { + final ToStringVisitor visitor = ToStringVisitor(uiAndFlutter); + final MockProcedure procedure = MockProcedure(); + final MockFunctionNode function = MockFunctionNode(); + final MockStatement statement = MockStatement(); + final Library library = Library(Uri.parse('package:flutter/src/foundation.dart')); + final Name name = Name('toString'); + + when(procedure.function).thenReturn(function); + when(procedure.name).thenReturn(name); + when(procedure.annotations).thenReturn(const []); + when(procedure.enclosingLibrary).thenReturn(library); + when(procedure.enclosingClass).thenReturn(Class()); + when(procedure.isAbstract).thenReturn(false); + when(procedure.isStatic).thenReturn(false); + when(function.body).thenReturn(statement); + + visitor.visitProcedure(procedure); + _validateReplacement(statement); + }); + + group('Integration tests', () { + final String dart = Platform.resolvedExecutable; + final String frontendServer = args[0]; + final String sdkRoot = args[1]; + final String basePath = path.canonicalize(path.join(path.dirname(Platform.script.path), '..')); + final String fixtures = path.join(basePath, 'test', 'fixtures'); + final String mainDart = path.join(fixtures, 'lib', 'main.dart'); + final String dotPackages = path.join(fixtures, '.packages'); + final String regularDill = path.join(fixtures, 'toString.dill'); + final String transformedDill = path.join(fixtures, 'toStringTransformed.dill'); + + + void _checkProcessResult(ProcessResult result) { + if (result.exitCode != 0) { + stdout.writeln(result.stdout); + stderr.writeln(result.stderr); + } + expect(result.exitCode, 0); + } + + test('Without flag', () async { + _checkProcessResult(Process.runSync(dart, [ + frontendServer, + '--sdk-root=$sdkRoot', + '--target=flutter', + '--packages=$dotPackages', + '--output-dill=$regularDill', + mainDart, + ])); + final ProcessResult runResult = Process.runSync(dart, [regularDill]); + _checkProcessResult(runResult); + String paintString = '"Paint.toString":"Paint(Color(0xffffffff))"'; + if (const bool.fromEnvironment('dart.vm.product', defaultValue: false)) { + paintString = '"Paint.toString":"Instance of \'Paint\'"'; + } + expect( + runResult.stdout.trim(), + '{$paintString,' + '"Brightness.toString":"Brightness.dark",' + '"Foo.toString":"I am a Foo",' + '"Keep.toString":"I am a Keep"}', + ); + }); + + test('With flag', () async { + _checkProcessResult(Process.runSync(dart, [ + frontendServer, + '--sdk-root=$sdkRoot', + '--target=flutter', + '--packages=$dotPackages', + '--output-dill=$transformedDill', + '--delete-tostring-package-uri', 'dart:ui', + '--delete-tostring-package-uri', 'package:flutter_frontend_fixtures', + mainDart, + ])); + final ProcessResult runResult = Process.runSync(dart, [transformedDill]); + _checkProcessResult(runResult); + expect( + runResult.stdout.trim(), + '{"Paint.toString":"Instance of \'Paint\'",' + '"Brightness.toString":"Brightness.dark",' + '"Foo.toString":"Instance of \'Foo\'",' + '"Keep.toString":"I am a Keep"}', + ); + }); + }); +} + +class MockComponent extends Mock implements Component {} +class MockTransformer extends Mock implements frontend.ProgramTransformer {} +class MockProcedure extends Mock implements Procedure {} +class MockFunctionNode extends Mock implements FunctionNode {} +class MockStatement extends Mock implements Statement {} diff --git a/fml/BUILD.gn b/fml/BUILD.gn index 356b8e2baa79d..f72ac64bc6a6f 100644 --- a/fml/BUILD.gn +++ b/fml/BUILD.gn @@ -10,6 +10,8 @@ import("//flutter/testing/testing.gni") source_set("fml") { sources = [ + "ascii_trie.cc", + "ascii_trie.h", "backtrace.h", "base32.cc", "base32.h", @@ -25,8 +27,6 @@ source_set("fml") { "eintr_wrapper.h", "file.cc", "file.h", - "gpu_thread_merger.cc", - "gpu_thread_merger.h", "hash_combine.h", "icu_util.cc", "icu_util.h", @@ -59,6 +59,8 @@ source_set("fml") { "paths.cc", "paths.h", "posix_wrappers.h", + "raster_thread_merger.cc", + "raster_thread_merger.h", "size.h", "synchronization/atomic_object.h", "synchronization/count_down_latch.cc", @@ -118,6 +120,9 @@ source_set("fml") { } if (is_ios || is_mac) { + cflags_objc = flutter_cflags_objc + cflags_objcc = flutter_cflags_objcc + sources += [ "platform/darwin/cf_utils.cc", "platform/darwin/cf_utils.h", @@ -178,25 +183,14 @@ source_set("fml") { "platform/fuchsia/paths_fuchsia.cc", ] - if (using_fuchsia_sdk) { - public_deps += [ - "$fuchsia_sdk_root/pkg:async-cpp", - "$fuchsia_sdk_root/pkg:async-loop-cpp", - "$fuchsia_sdk_root/pkg:async-loop-default", - "$fuchsia_sdk_root/pkg:trace", - "$fuchsia_sdk_root/pkg:trace-engine", - "$fuchsia_sdk_root/pkg:zx", - ] - } else { - public_deps += [ - "//zircon/public/lib/async-cpp", - "//zircon/public/lib/async-loop-cpp", - "//zircon/public/lib/async-loop-default", - "//zircon/public/lib/trace", - "//zircon/public/lib/trace-engine", - "//zircon/public/lib/zx", - ] - } + public_deps += [ + "$fuchsia_sdk_root/pkg:async-cpp", + "$fuchsia_sdk_root/pkg:async-loop-cpp", + "$fuchsia_sdk_root/pkg:async-loop-default", + "$fuchsia_sdk_root/pkg:trace", + "$fuchsia_sdk_root/pkg:trace-engine", + "$fuchsia_sdk_root/pkg:zx", + ] } if (is_win) { @@ -236,11 +230,11 @@ executable("fml_unittests") { testonly = true sources = [ + "ascii_trie_unittests.cc", "backtrace_unittests.cc", "base32_unittest.cc", "command_line_unittest.cc", "file_unittest.cc", - "gpu_thread_merger_unittests.cc", "hash_combine_unittests.cc", "memory/ref_counted_unittest.cc", "memory/weak_ptr_unittest.cc", @@ -249,7 +243,7 @@ executable("fml_unittests") { "message_loop_unittests.cc", "message_unittests.cc", "paths_unittests.cc", - "platform/darwin/string_range_sanitization_unittests.mm", + "raster_thread_merger_unittests.cc", "synchronization/count_down_latch_unittests.cc", "synchronization/semaphore_unittest.cc", "synchronization/sync_switch_unittest.cc", @@ -261,6 +255,13 @@ executable("fml_unittests") { "time/time_unittest.cc", ] + if (is_mac) { + sources += [ + "platform/darwin/cf_utils_unittests.mm", + "platform/darwin/string_range_sanitization_unittests.mm", + ] + } + deps = [ ":fml_fixtures", "//flutter/fml", @@ -269,7 +270,7 @@ executable("fml_unittests") { "//flutter/testing", ] - if (is_fuchsia && using_fuchsia_sdk) { + if (is_fuchsia) { libs = [ "${fuchsia_sdk_path}/arch/${target_cpu}/sysroot/lib/libzircon.so" ] } } diff --git a/fml/ascii_trie.cc b/fml/ascii_trie.cc new file mode 100644 index 0000000000000..57892591fca33 --- /dev/null +++ b/fml/ascii_trie.cc @@ -0,0 +1,48 @@ +// 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/fml/ascii_trie.h" +#include "flutter/fml/logging.h" + +namespace fml { +typedef AsciiTrie::TrieNode TrieNode; +typedef AsciiTrie::TrieNodePtr TrieNodePtr; + +namespace { +void Add(TrieNodePtr* trie, const char* entry) { + int ch = entry[0]; + FML_DCHECK(ch < AsciiTrie::kMaxAsciiValue); + if (ch != 0) { + if (!*trie) { + *trie = std::make_unique(); + } + Add(&(*trie)->children[ch], entry + 1); + } +} + +TrieNodePtr MakeTrie(const std::vector& entries) { + TrieNodePtr result; + for (const std::string& entry : entries) { + Add(&result, entry.c_str()); + } + return result; +} +} // namespace + +void AsciiTrie::Fill(const std::vector& entries) { + node_ = MakeTrie(entries); +} + +bool AsciiTrie::Query(TrieNode* trie, const char* query) { + FML_DCHECK(trie); + const char* char_position = query; + TrieNode* trie_position = trie; + TrieNode* child; + int ch; + while ((ch = *char_position) && (child = trie_position->children[ch].get())) { + char_position++; + trie_position = child; + } + return !child && trie_position != trie; +} +} // namespace fml diff --git a/fml/ascii_trie.h b/fml/ascii_trie.h new file mode 100644 index 0000000000000..e6ff430a08e4f --- /dev/null +++ b/fml/ascii_trie.h @@ -0,0 +1,39 @@ +// 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_FML_ASCIITRIE_H_ +#define FLUTTER_FML_ASCIITRIE_H_ + +#include +#include + +namespace fml { + +/// A trie for looking for ASCII prefixes. +class AsciiTrie { + public: + struct TrieNode; + typedef std::unique_ptr TrieNodePtr; + /// The max Ascii value. + static const int kMaxAsciiValue = 128; + + /// Clear and insert all the entries into the trie. + void Fill(const std::vector& entries); + + /// Returns true if \p argument is prefixed by the contents of the trie. + inline bool Query(const char* argument) { + return !node_ || Query(node_.get(), argument); + } + + struct TrieNode { + TrieNodePtr children[kMaxAsciiValue]; + }; + + private: + static bool Query(TrieNode* trie, const char* query); + TrieNodePtr node_; +}; +} // namespace fml + +#endif diff --git a/fml/ascii_trie_unittests.cc b/fml/ascii_trie_unittests.cc new file mode 100644 index 0000000000000..e11cb185d28bb --- /dev/null +++ b/fml/ascii_trie_unittests.cc @@ -0,0 +1,36 @@ +// 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/fml/ascii_trie.h" +#include "gtest/gtest.h" + +using fml::AsciiTrie; + +TEST(AsciiTableTest, Simple) { + AsciiTrie trie; + auto entries = std::vector{"foo"}; + trie.Fill(entries); + ASSERT_TRUE(trie.Query("foobar")); + ASSERT_FALSE(trie.Query("google")); +} + +TEST(AsciiTableTest, ExactMatch) { + AsciiTrie trie; + auto entries = std::vector{"foo"}; + trie.Fill(entries); + ASSERT_TRUE(trie.Query("foo")); +} + +TEST(AsciiTableTest, Empty) { + AsciiTrie trie; + ASSERT_TRUE(trie.Query("foo")); +} + +TEST(AsciiTableTest, MultipleEntries) { + AsciiTrie trie; + auto entries = std::vector{"foo", "bar"}; + trie.Fill(entries); + ASSERT_TRUE(trie.Query("foozzz")); + ASSERT_TRUE(trie.Query("barzzz")); +} diff --git a/fml/log_settings.cc b/fml/log_settings.cc index d70525a72e089..057de5351e453 100644 --- a/fml/log_settings.cc +++ b/fml/log_settings.cc @@ -34,4 +34,13 @@ int GetMinLogLevel() { return std::min(state::g_log_settings.min_log_level, LOG_FATAL); } +ScopedSetLogSettings::ScopedSetLogSettings(const LogSettings& settings) { + old_settings_ = GetLogSettings(); + SetLogSettings(settings); +} + +ScopedSetLogSettings::~ScopedSetLogSettings() { + SetLogSettings(old_settings_); +} + } // namespace fml diff --git a/fml/log_settings.h b/fml/log_settings.h index 2cd14e028c313..cce9ce7bd90d7 100644 --- a/fml/log_settings.h +++ b/fml/log_settings.h @@ -35,6 +35,15 @@ LogSettings GetLogSettings(); // higher than LOG_FATAL. int GetMinLogLevel(); +class ScopedSetLogSettings { + public: + ScopedSetLogSettings(const LogSettings& settings); + ~ScopedSetLogSettings(); + + private: + LogSettings old_settings_; +}; + } // namespace fml #endif // FLUTTER_FML_LOG_SETTINGS_H_ diff --git a/fml/platform/darwin/cf_utils.h b/fml/platform/darwin/cf_utils.h index f6be04abaaa71..00ed82d1f4411 100644 --- a/fml/platform/darwin/cf_utils.h +++ b/fml/platform/darwin/cf_utils.h @@ -18,6 +18,21 @@ class CFRef { CFRef(T instance) : instance_(instance) {} + CFRef(const CFRef& other) : instance_(other.instance_) { + if (instance_) { + CFRetain(instance_); + } + } + + CFRef(CFRef&& other) : instance_(other.instance_) { + other.instance_ = nullptr; + } + + CFRef& operator=(CFRef&& other) { + Reset(other.Release()); + return *this; + } + ~CFRef() { if (instance_ != nullptr) { CFRelease(instance_); @@ -25,7 +40,7 @@ class CFRef { instance_ = nullptr; } - void Reset(T instance) { + void Reset(T instance = nullptr) { if (instance_ == instance) { return; } @@ -36,6 +51,12 @@ class CFRef { instance_ = instance; } + [[nodiscard]] T Release() { + auto instance = instance_; + instance_ = nullptr; + return instance; + } + operator T() const { return instance_; } operator bool() const { return instance_ != nullptr; } @@ -43,7 +64,7 @@ class CFRef { private: T instance_; - FML_DISALLOW_COPY_AND_ASSIGN(CFRef); + CFRef& operator=(const CFRef&) = delete; }; } // namespace fml diff --git a/fml/platform/darwin/cf_utils_unittests.mm b/fml/platform/darwin/cf_utils_unittests.mm new file mode 100644 index 0000000000000..9df1f5f6ddfd3 --- /dev/null +++ b/fml/platform/darwin/cf_utils_unittests.mm @@ -0,0 +1,63 @@ +// 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/fml/platform/darwin/cf_utils.h" +#include "flutter/testing/testing.h" + +namespace fml { +namespace testing { + +TEST(CFTest, CanCreateRefs) { + CFRef string(CFStringCreateMutable(kCFAllocatorDefault, 100u)); + // Cast + ASSERT_TRUE(static_cast(string)); + ASSERT_TRUE(string); + + const auto ref_count = CFGetRetainCount(string); + + // Copy & Reset + { + CFRef string2 = string; + ASSERT_TRUE(string2); + ASSERT_EQ(ref_count + 1u, CFGetRetainCount(string)); + ASSERT_EQ(CFGetRetainCount(string2), CFGetRetainCount(string)); + + string2.Reset(); + ASSERT_FALSE(string2); + ASSERT_EQ(ref_count, CFGetRetainCount(string)); + } + + // Release + { + auto string3 = string; + ASSERT_TRUE(string3); + ASSERT_EQ(ref_count + 1u, CFGetRetainCount(string)); + auto raw_string3 = string3.Release(); + ASSERT_FALSE(string3); + ASSERT_EQ(ref_count + 1u, CFGetRetainCount(string)); + CFRelease(raw_string3); + ASSERT_EQ(ref_count, CFGetRetainCount(string)); + } + + // Move + { + auto string_source = string; + ASSERT_TRUE(string_source); + auto string_move = std::move(string_source); + ASSERT_FALSE(string_source); + ASSERT_EQ(ref_count + 1u, CFGetRetainCount(string)); + string_move.Reset(); + ASSERT_EQ(ref_count, CFGetRetainCount(string)); + } + + // Move assign. + { + auto string_move_assign = std::move(string); + ASSERT_FALSE(string); + ASSERT_EQ(ref_count, CFGetRetainCount(string_move_assign)); + } +} + +} // namespace testing +} // namespace fml diff --git a/fml/gpu_thread_merger.cc b/fml/raster_thread_merger.cc similarity index 65% rename from fml/gpu_thread_merger.cc rename to fml/raster_thread_merger.cc index 9d0c2415b6a09..718b41ce327d3 100644 --- a/fml/gpu_thread_merger.cc +++ b/fml/raster_thread_merger.cc @@ -4,15 +4,15 @@ #define FML_USED_ON_EMBEDDER -#include "flutter/fml/gpu_thread_merger.h" +#include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/message_loop_impl.h" namespace fml { -const int GpuThreadMerger::kLeaseNotSet = -1; +const int RasterThreadMerger::kLeaseNotSet = -1; -GpuThreadMerger::GpuThreadMerger(fml::TaskQueueId platform_queue_id, - fml::TaskQueueId gpu_queue_id) +RasterThreadMerger::RasterThreadMerger(fml::TaskQueueId platform_queue_id, + fml::TaskQueueId gpu_queue_id) : platform_queue_id_(platform_queue_id), gpu_queue_id_(gpu_queue_id), task_queues_(fml::MessageLoopTaskQueues::GetInstance()), @@ -20,7 +20,7 @@ GpuThreadMerger::GpuThreadMerger(fml::TaskQueueId platform_queue_id, is_merged_ = task_queues_->Owns(platform_queue_id_, gpu_queue_id_); } -void GpuThreadMerger::MergeWithLease(size_t lease_term) { +void RasterThreadMerger::MergeWithLease(size_t lease_term) { FML_DCHECK(lease_term > 0) << "lease_term should be positive."; if (!is_merged_) { is_merged_ = task_queues_->Merge(platform_queue_id_, gpu_queue_id_); @@ -28,7 +28,7 @@ void GpuThreadMerger::MergeWithLease(size_t lease_term) { } } -bool GpuThreadMerger::IsOnRasterizingThread() { +bool RasterThreadMerger::IsOnRasterizingThread() { const auto current_queue_id = MessageLoop::GetCurrentTaskQueueId(); if (is_merged_) { return current_queue_id == platform_queue_id_; @@ -37,25 +37,25 @@ bool GpuThreadMerger::IsOnRasterizingThread() { } } -void GpuThreadMerger::ExtendLeaseTo(size_t lease_term) { +void RasterThreadMerger::ExtendLeaseTo(size_t lease_term) { FML_DCHECK(lease_term > 0) << "lease_term should be positive."; if (lease_term_ != kLeaseNotSet && (int)lease_term > lease_term_) { lease_term_ = lease_term; } } -bool GpuThreadMerger::IsMerged() const { +bool RasterThreadMerger::IsMerged() const { return is_merged_; } -GpuThreadStatus GpuThreadMerger::DecrementLease() { +RasterThreadStatus RasterThreadMerger::DecrementLease() { if (!is_merged_) { - return GpuThreadStatus::kRemainsUnmerged; + return RasterThreadStatus::kRemainsUnmerged; } // we haven't been set to merge. if (lease_term_ == kLeaseNotSet) { - return GpuThreadStatus::kRemainsUnmerged; + return RasterThreadStatus::kRemainsUnmerged; } FML_DCHECK(lease_term_ > 0) @@ -63,12 +63,12 @@ GpuThreadStatus GpuThreadMerger::DecrementLease() { lease_term_--; if (lease_term_ == 0) { bool success = task_queues_->Unmerge(platform_queue_id_); - FML_CHECK(success) << "Unable to un-merge the GPU and platform threads."; + FML_CHECK(success) << "Unable to un-merge the raster and platform threads."; is_merged_ = false; - return GpuThreadStatus::kUnmergedNow; + return RasterThreadStatus::kUnmergedNow; } - return GpuThreadStatus::kRemainsMerged; + return RasterThreadStatus::kRemainsMerged; } } // namespace fml diff --git a/fml/gpu_thread_merger.h b/fml/raster_thread_merger.h similarity index 63% rename from fml/gpu_thread_merger.h rename to fml/raster_thread_merger.h index 4b7cb5ae2e5df..1e12e3c41d9b8 100644 --- a/fml/gpu_thread_merger.h +++ b/fml/raster_thread_merger.h @@ -13,11 +13,16 @@ namespace fml { class MessageLoopImpl; -enum class GpuThreadStatus { kRemainsMerged, kRemainsUnmerged, kUnmergedNow }; +enum class RasterThreadStatus { + kRemainsMerged, + kRemainsUnmerged, + kUnmergedNow +}; -class GpuThreadMerger : public fml::RefCountedThreadSafe { +class RasterThreadMerger + : public fml::RefCountedThreadSafe { public: - // Merges the GPU thread into platform thread for the duration of + // Merges the raster thread into platform thread for the duration of // the lease term. Lease is managed by the caller by either calling // |ExtendLeaseTo| or |DecrementLease|. // When the caller merges with a lease term of say 2. The threads @@ -27,18 +32,18 @@ class GpuThreadMerger : public fml::RefCountedThreadSafe { void ExtendLeaseTo(size_t lease_term); - // Returns |GpuThreadStatus::kUnmergedNow| if this call resulted in splitting - // the GPU and platform threads. Reduces the lease term by 1. - GpuThreadStatus DecrementLease(); + // Returns |RasterThreadStatus::kUnmergedNow| if this call resulted in + // splitting the raster and platform threads. Reduces the lease term by 1. + RasterThreadStatus DecrementLease(); bool IsMerged() const; - GpuThreadMerger(fml::TaskQueueId platform_queue_id, - fml::TaskQueueId gpu_queue_id); + RasterThreadMerger(fml::TaskQueueId platform_queue_id, + fml::TaskQueueId gpu_queue_id); // Returns true if the current thread owns rasterizing. // When the threads are merged, platform thread owns rasterizing. - // When un-merged, gpu thread owns rasterizing. + // When un-merged, raster thread owns rasterizing. bool IsOnRasterizingThread(); private: @@ -49,9 +54,9 @@ class GpuThreadMerger : public fml::RefCountedThreadSafe { std::atomic_int lease_term_; bool is_merged_; - FML_FRIEND_REF_COUNTED_THREAD_SAFE(GpuThreadMerger); - FML_FRIEND_MAKE_REF_COUNTED(GpuThreadMerger); - FML_DISALLOW_COPY_AND_ASSIGN(GpuThreadMerger); + FML_FRIEND_REF_COUNTED_THREAD_SAFE(RasterThreadMerger); + FML_FRIEND_MAKE_REF_COUNTED(RasterThreadMerger); + FML_DISALLOW_COPY_AND_ASSIGN(RasterThreadMerger); }; } // namespace fml diff --git a/fml/gpu_thread_merger_unittests.cc b/fml/raster_thread_merger_unittests.cc similarity index 73% rename from fml/gpu_thread_merger_unittests.cc rename to fml/raster_thread_merger_unittests.cc index 66f21c0fc494d..40ae9ba5351e2 100644 --- a/fml/gpu_thread_merger_unittests.cc +++ b/fml/raster_thread_merger_unittests.cc @@ -7,14 +7,14 @@ #include #include -#include "flutter/fml/gpu_thread_merger.h" #include "flutter/fml/message_loop.h" +#include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/synchronization/count_down_latch.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/fml/task_runner.h" #include "gtest/gtest.h" -TEST(GpuThreadMerger, RemainMergedTillLeaseExpires) { +TEST(RasterThreadMerger, RemainMergedTillLeaseExpires) { fml::MessageLoop* loop1 = nullptr; fml::AutoResetWaitableEvent latch1; fml::AutoResetWaitableEvent term1; @@ -40,20 +40,20 @@ TEST(GpuThreadMerger, RemainMergedTillLeaseExpires) { fml::TaskQueueId qid1 = loop1->GetTaskRunner()->GetTaskQueueId(); fml::TaskQueueId qid2 = loop2->GetTaskRunner()->GetTaskQueueId(); - const auto gpu_thread_merger_ = - fml::MakeRefCounted(qid1, qid2); + const auto raster_thread_merger_ = + fml::MakeRefCounted(qid1, qid2); const int kNumFramesMerged = 5; - ASSERT_FALSE(gpu_thread_merger_->IsMerged()); + ASSERT_FALSE(raster_thread_merger_->IsMerged()); - gpu_thread_merger_->MergeWithLease(kNumFramesMerged); + raster_thread_merger_->MergeWithLease(kNumFramesMerged); for (int i = 0; i < kNumFramesMerged; i++) { - ASSERT_TRUE(gpu_thread_merger_->IsMerged()); - gpu_thread_merger_->DecrementLease(); + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + raster_thread_merger_->DecrementLease(); } - ASSERT_FALSE(gpu_thread_merger_->IsMerged()); + ASSERT_FALSE(raster_thread_merger_->IsMerged()); term1.Signal(); term2.Signal(); @@ -61,7 +61,7 @@ TEST(GpuThreadMerger, RemainMergedTillLeaseExpires) { thread2.join(); } -TEST(GpuThreadMerger, IsNotOnRasterizingThread) { +TEST(RasterThreadMerger, IsNotOnRasterizingThread) { fml::MessageLoop* loop1 = nullptr; fml::AutoResetWaitableEvent latch1; std::thread thread1([&loop1, &latch1]() { @@ -85,29 +85,29 @@ TEST(GpuThreadMerger, IsNotOnRasterizingThread) { fml::TaskQueueId qid1 = loop1->GetTaskRunner()->GetTaskQueueId(); fml::TaskQueueId qid2 = loop2->GetTaskRunner()->GetTaskQueueId(); - const auto gpu_thread_merger_ = - fml::MakeRefCounted(qid1, qid2); + const auto raster_thread_merger_ = + fml::MakeRefCounted(qid1, qid2); fml::CountDownLatch pre_merge(2), post_merge(2), post_unmerge(2); loop1->GetTaskRunner()->PostTask([&]() { - ASSERT_FALSE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_FALSE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid1); pre_merge.CountDown(); }); loop2->GetTaskRunner()->PostTask([&]() { - ASSERT_TRUE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_TRUE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid2); pre_merge.CountDown(); }); pre_merge.Wait(); - gpu_thread_merger_->MergeWithLease(1); + raster_thread_merger_->MergeWithLease(1); loop1->GetTaskRunner()->PostTask([&]() { - ASSERT_TRUE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_TRUE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid1); post_merge.CountDown(); }); @@ -115,23 +115,23 @@ TEST(GpuThreadMerger, IsNotOnRasterizingThread) { loop2->GetTaskRunner()->PostTask([&]() { // this will be false since this is going to be run // on loop1 really. - ASSERT_TRUE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_TRUE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid1); post_merge.CountDown(); }); post_merge.Wait(); - gpu_thread_merger_->DecrementLease(); + raster_thread_merger_->DecrementLease(); loop1->GetTaskRunner()->PostTask([&]() { - ASSERT_FALSE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_FALSE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid1); post_unmerge.CountDown(); }); loop2->GetTaskRunner()->PostTask([&]() { - ASSERT_TRUE(gpu_thread_merger_->IsOnRasterizingThread()); + ASSERT_TRUE(raster_thread_merger_->IsOnRasterizingThread()); ASSERT_EQ(fml::MessageLoop::GetCurrentTaskQueueId(), qid2); post_unmerge.CountDown(); }); @@ -146,7 +146,7 @@ TEST(GpuThreadMerger, IsNotOnRasterizingThread) { thread2.join(); } -TEST(GpuThreadMerger, LeaseExtension) { +TEST(RasterThreadMerger, LeaseExtension) { fml::MessageLoop* loop1 = nullptr; fml::AutoResetWaitableEvent latch1; fml::AutoResetWaitableEvent term1; @@ -172,30 +172,30 @@ TEST(GpuThreadMerger, LeaseExtension) { fml::TaskQueueId qid1 = loop1->GetTaskRunner()->GetTaskQueueId(); fml::TaskQueueId qid2 = loop2->GetTaskRunner()->GetTaskQueueId(); - const auto gpu_thread_merger_ = - fml::MakeRefCounted(qid1, qid2); + const auto raster_thread_merger_ = + fml::MakeRefCounted(qid1, qid2); const int kNumFramesMerged = 5; - ASSERT_FALSE(gpu_thread_merger_->IsMerged()); + ASSERT_FALSE(raster_thread_merger_->IsMerged()); - gpu_thread_merger_->MergeWithLease(kNumFramesMerged); + raster_thread_merger_->MergeWithLease(kNumFramesMerged); // let there be one more turn till the leases expire. for (int i = 0; i < kNumFramesMerged - 1; i++) { - ASSERT_TRUE(gpu_thread_merger_->IsMerged()); - gpu_thread_merger_->DecrementLease(); + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + raster_thread_merger_->DecrementLease(); } // extend the lease once. - gpu_thread_merger_->ExtendLeaseTo(kNumFramesMerged); + raster_thread_merger_->ExtendLeaseTo(kNumFramesMerged); // we will NOT last for 1 extra turn, we just set it. for (int i = 0; i < kNumFramesMerged; i++) { - ASSERT_TRUE(gpu_thread_merger_->IsMerged()); - gpu_thread_merger_->DecrementLease(); + ASSERT_TRUE(raster_thread_merger_->IsMerged()); + raster_thread_merger_->DecrementLease(); } - ASSERT_FALSE(gpu_thread_merger_->IsMerged()); + ASSERT_FALSE(raster_thread_merger_->IsMerged()); term1.Signal(); term2.Signal(); diff --git a/fml/time/time_delta.h b/fml/time/time_delta.h index 98a11b4100800..e54a1f8bf9bfa 100644 --- a/fml/time/time_delta.h +++ b/fml/time/time_delta.h @@ -55,6 +55,10 @@ class TimeDelta { return FromNanoseconds(seconds * (1000.0 * 1000.0 * 1000.0)); } + static constexpr TimeDelta FromMillisecondsF(double millis) { + return FromNanoseconds(millis * (1000.0 * 1000.0)); + } + constexpr int64_t ToNanoseconds() const { return delta_; } constexpr int64_t ToMicroseconds() const { return ToNanoseconds() / 1000; } constexpr int64_t ToMilliseconds() const { return ToMicroseconds() / 1000; } diff --git a/fml/trace_event.cc b/fml/trace_event.cc index 8698a2b0f203e..d055582d3a0eb 100644 --- a/fml/trace_event.cc +++ b/fml/trace_event.cc @@ -8,6 +8,7 @@ #include #include +#include "flutter/fml/ascii_trie.h" #include "flutter/fml/build_config.h" #include "flutter/fml/logging.h" @@ -22,6 +23,27 @@ namespace tracing { #if TIMELINE_ENABLED +namespace { +AsciiTrie gWhitelist; + +inline void FlutterTimelineEvent(const char* label, + int64_t timestamp0, + int64_t timestamp1_or_async_id, + Dart_Timeline_Event_Type type, + intptr_t argument_count, + const char** argument_names, + const char** argument_values) { + if (gWhitelist.Query(label)) { + Dart_TimelineEvent(label, timestamp0, timestamp1_or_async_id, type, + argument_count, argument_names, argument_values); + } +} +} // namespace + +void TraceSetWhitelist(const std::vector& whitelist) { + gWhitelist.Fill(whitelist); +} + size_t TraceNonce() { static std::atomic_size_t gLastItem; return ++gLastItem; @@ -29,6 +51,7 @@ size_t TraceNonce() { void TraceTimelineEvent(TraceArg category_group, TraceArg name, + int64_t timestamp_micros, TraceIDArg identifier, Dart_Timeline_Event_Type type, const std::vector& c_names, @@ -42,9 +65,9 @@ void TraceTimelineEvent(TraceArg category_group, c_values[i] = values[i].c_str(); } - Dart_TimelineEvent( + FlutterTimelineEvent( name, // label - Dart_TimelineGetMicros(), // timestamp0 + timestamp_micros, // timestamp0 identifier, // timestamp1_or_async_id type, // event type argument_count, // argument_count @@ -53,14 +76,30 @@ void TraceTimelineEvent(TraceArg category_group, ); } +void TraceTimelineEvent(TraceArg category_group, + TraceArg name, + TraceIDArg identifier, + Dart_Timeline_Event_Type type, + const std::vector& c_names, + const std::vector& values) { + TraceTimelineEvent(category_group, // group + name, // name + Dart_TimelineGetMicros(), // timestamp_micros + identifier, // identifier + type, // type + c_names, // names + values // values + ); +} + void TraceEvent0(TraceArg category_group, TraceArg name) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - 0, // timestamp1_or_async_id - Dart_Timeline_Event_Begin, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + 0, // timestamp1_or_async_id + Dart_Timeline_Event_Begin, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } @@ -70,13 +109,13 @@ void TraceEvent1(TraceArg category_group, TraceArg arg1_val) { const char* arg_names[] = {arg1_name}; const char* arg_values[] = {arg1_val}; - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - 0, // timestamp1_or_async_id - Dart_Timeline_Event_Begin, // event type - 1, // argument_count - arg_names, // argument_names - arg_values // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + 0, // timestamp1_or_async_id + Dart_Timeline_Event_Begin, // event type + 1, // argument_count + arg_names, // argument_names + arg_values // argument_values ); } @@ -88,78 +127,50 @@ void TraceEvent2(TraceArg category_group, TraceArg arg2_val) { const char* arg_names[] = {arg1_name, arg2_name}; const char* arg_values[] = {arg1_val, arg2_val}; - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - 0, // timestamp1_or_async_id - Dart_Timeline_Event_Begin, // event type - 2, // argument_count - arg_names, // argument_names - arg_values // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + 0, // timestamp1_or_async_id + Dart_Timeline_Event_Begin, // event type + 2, // argument_count + arg_names, // argument_names + arg_values // argument_values ); } void TraceEventEnd(TraceArg name) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - 0, // timestamp1_or_async_id - Dart_Timeline_Event_End, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values - ); -} - -void TraceEventAsyncComplete(TraceArg category_group, - TraceArg name, - TimePoint begin, - TimePoint end) { - auto identifier = TraceNonce(); - - if (begin > end) { - std::swap(begin, end); - } - - Dart_TimelineEvent(name, // label - begin.ToEpochDelta().ToMicroseconds(), // timestamp0 - identifier, // timestamp1_or_async_id - Dart_Timeline_Event_Async_Begin, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values - ); - Dart_TimelineEvent(name, // label - end.ToEpochDelta().ToMicroseconds(), // timestamp0 - identifier, // timestamp1_or_async_id - Dart_Timeline_Event_Async_End, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + 0, // timestamp1_or_async_id + Dart_Timeline_Event_End, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } void TraceEventAsyncBegin0(TraceArg category_group, TraceArg name, TraceIDArg id) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Async_Begin, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Async_Begin, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } void TraceEventAsyncEnd0(TraceArg category_group, TraceArg name, TraceIDArg id) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Async_End, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Async_End, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } @@ -170,13 +181,13 @@ void TraceEventAsyncBegin1(TraceArg category_group, TraceArg arg1_val) { const char* arg_names[] = {arg1_name}; const char* arg_values[] = {arg1_val}; - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Async_Begin, // event type - 1, // argument_count - arg_names, // argument_names - arg_values // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Async_Begin, // event type + 1, // argument_count + arg_names, // argument_names + arg_values // argument_values ); } @@ -187,70 +198,80 @@ void TraceEventAsyncEnd1(TraceArg category_group, TraceArg arg1_val) { const char* arg_names[] = {arg1_name}; const char* arg_values[] = {arg1_val}; - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Async_End, // event type - 1, // argument_count - arg_names, // argument_names - arg_values // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Async_End, // event type + 1, // argument_count + arg_names, // argument_names + arg_values // argument_values ); } void TraceEventInstant0(TraceArg category_group, TraceArg name) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - 0, // timestamp1_or_async_id - Dart_Timeline_Event_Instant, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + 0, // timestamp1_or_async_id + Dart_Timeline_Event_Instant, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } void TraceEventFlowBegin0(TraceArg category_group, TraceArg name, TraceIDArg id) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Flow_Begin, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Flow_Begin, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } void TraceEventFlowStep0(TraceArg category_group, TraceArg name, TraceIDArg id) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Flow_Step, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Flow_Step, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } void TraceEventFlowEnd0(TraceArg category_group, TraceArg name, TraceIDArg id) { - Dart_TimelineEvent(name, // label - Dart_TimelineGetMicros(), // timestamp0 - id, // timestamp1_or_async_id - Dart_Timeline_Event_Flow_End, // event type - 0, // argument_count - nullptr, // argument_names - nullptr // argument_values + FlutterTimelineEvent(name, // label + Dart_TimelineGetMicros(), // timestamp0 + id, // timestamp1_or_async_id + Dart_Timeline_Event_Flow_End, // event type + 0, // argument_count + nullptr, // argument_names + nullptr // argument_values ); } #else // TIMELINE_ENABLED +void TraceSetWhitelist(const std::vector& whitelist) {} + size_t TraceNonce() { return 0; } +void TraceTimelineEvent(TraceArg category_group, + TraceArg name, + int64_t timestamp_micros, + TraceIDArg identifier, + Dart_Timeline_Event_Type type, + const std::vector& c_names, + const std::vector& values) {} + void TraceTimelineEvent(TraceArg category_group, TraceArg name, TraceIDArg identifier, diff --git a/fml/trace_event.h b/fml/trace_event.h index a5b478ffc150e..98d328e134819 100644 --- a/fml/trace_event.h +++ b/fml/trace_event.h @@ -112,6 +112,16 @@ namespace tracing { using TraceArg = const char*; using TraceIDArg = int64_t; +void TraceSetWhitelist(const std::vector& whitelist); + +void TraceTimelineEvent(TraceArg category_group, + TraceArg name, + int64_t timestamp_micros, + TraceIDArg id, + Dart_Timeline_Event_Type type, + const std::vector& names, + const std::vector& values); + void TraceTimelineEvent(TraceArg category_group, TraceArg name, TraceIDArg id, @@ -207,10 +217,40 @@ void TraceEvent2(TraceArg category_group, void TraceEventEnd(TraceArg name); +template void TraceEventAsyncComplete(TraceArg category_group, TraceArg name, TimePoint begin, - TimePoint end); + TimePoint end, + Args... args) { + auto identifier = TraceNonce(); + const auto split = SplitArguments(args...); + + if (begin > end) { + std::swap(begin, end); + } + + const int64_t begin_micros = begin.ToEpochDelta().ToMicroseconds(); + const int64_t end_micros = end.ToEpochDelta().ToMicroseconds(); + + TraceTimelineEvent(category_group, // group + name, // name + begin_micros, // timestamp_micros + identifier, // identifier + Dart_Timeline_Event_Async_Begin, // type + split.first, // names + split.second // values + ); + + TraceTimelineEvent(category_group, // group + name, // name + end_micros, // timestamp_micros + identifier, // identifier + Dart_Timeline_Event_Async_End, // type + split.first, // names + split.second // values + ); +} void TraceEventAsyncBegin0(TraceArg category_group, TraceArg name, diff --git a/lib/io/BUILD.gn b/lib/io/BUILD.gn index 27cdc99eaa429..529872c30d23c 100644 --- a/lib/io/BUILD.gn +++ b/lib/io/BUILD.gn @@ -9,6 +9,7 @@ source_set("io") { ] deps = [ + "//flutter/fml", "//flutter/third_party/tonic", "//third_party/dart/runtime:dart_api", "//third_party/dart/runtime/bin:dart_io_api", diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index 70dd8d1f1c630..6e5e538d74da0 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -4,21 +4,31 @@ #include "flutter/lib/io/dart_io.h" +#include "flutter/fml/logging.h" + #include "third_party/dart/runtime/include/bin/dart_io_api.h" #include "third_party/dart/runtime/include/dart_api.h" #include "third_party/tonic/converter/dart_converter.h" +#include "third_party/tonic/logging/dart_error.h" +using tonic::LogIfError; using tonic::ToDart; namespace flutter { -void DartIO::InitForIsolate() { +void DartIO::InitForIsolate(bool disable_http) { Dart_Handle result = Dart_SetNativeResolver( Dart_LookupLibrary(ToDart("dart:io")), dart::bin::LookupIONative, dart::bin::LookupIONativeSymbol); - if (Dart_IsError(result)) { - Dart_PropagateError(result); - } + FML_CHECK(!LogIfError(result)); + + // The SDK expects this field to represent "allow http" so we switch the + // value. + Dart_Handle allow_http_value = disable_http ? Dart_False() : Dart_True(); + Dart_Handle set_field_result = + Dart_SetField(Dart_LookupLibrary(ToDart("dart:_http")), + ToDart("_embedderAllowsHttp"), allow_http_value); + FML_CHECK(!LogIfError(set_field_result)); } } // namespace flutter diff --git a/lib/io/dart_io.h b/lib/io/dart_io.h index 10fe07b514744..27ce7aa65baeb 100644 --- a/lib/io/dart_io.h +++ b/lib/io/dart_io.h @@ -13,7 +13,7 @@ namespace flutter { class DartIO { public: - static void InitForIsolate(); + static void InitForIsolate(bool disable_http); private: FML_DISALLOW_IMPLICIT_CONSTRUCTORS(DartIO); diff --git a/lib/snapshot/BUILD.gn b/lib/snapshot/BUILD.gn index 41740be8f391a..f21f3dffdd2c6 100644 --- a/lib/snapshot/BUILD.gn +++ b/lib/snapshot/BUILD.gn @@ -30,12 +30,7 @@ compiled_action("generate_snapshot_bin") { tool = "//third_party/dart/runtime/bin:gen_snapshot" } - if ((is_fuchsia || is_fuchsia_host) && !using_fuchsia_sdk) { - platform_kernel = - "$root_out_dir/flutter_runner_patched_sdk/platform_strong.dill" - } else { - platform_kernel = "$root_out_dir/flutter_patched_sdk/platform_strong.dill" - } + platform_kernel = "$root_out_dir/flutter_patched_sdk/platform_strong.dill" inputs = [ platform_kernel, @@ -217,11 +212,7 @@ bin_to_linkable("platform_strong_dill_linkable") { deps = [ ":kernel_platform_files", ] - if ((is_fuchsia || is_fuchsia_host) && !using_fuchsia_sdk) { - input = "$root_out_dir/flutter_runner_patched_sdk/platform_strong.dill" - } else { - input = "$root_out_dir/flutter_patched_sdk/platform_strong.dill" - } + input = "$root_out_dir/flutter_patched_sdk/platform_strong.dill" symbol = "kPlatformStrongDill" size_symbol = "kPlatformStrongDillSize" executable = false @@ -283,16 +274,8 @@ compile_platform("strong_platform") { } # Fuchsia's snapshot requires a different platform with extra dart: libraries. -if ((is_fuchsia || is_fuchsia_host) && !using_fuchsia_sdk) { - group("kernel_platform_files") { - public_deps = [ - "//topaz/runtime/flutter_runner/kernel:kernel_platform_files", - ] - } -} else { - group("kernel_platform_files") { - public_deps = [ - ":strong_platform", - ] - } +group("kernel_platform_files") { + public_deps = [ + ":strong_platform", + ] } diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 4d3210d3bef37..a581708f03c24 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -135,15 +135,11 @@ source_set("ui") { "compositing/scene_host.h", ] - if (using_fuchsia_sdk) { - deps += [ - "$fuchsia_sdk_root/pkg:async-cpp", - "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", - "//flutter/shell/platform/fuchsia/dart-pkg/zircon", - ] - } else { - deps += [ "//topaz/public/dart-pkg/zircon" ] - } + deps += [ + "$fuchsia_sdk_root/pkg:async-cpp", + "//flutter/shell/platform/fuchsia/dart-pkg/fuchsia", + "//flutter/shell/platform/fuchsia/dart-pkg/zircon", + ] } } diff --git a/lib/ui/annotations.dart b/lib/ui/annotations.dart new file mode 100644 index 0000000000000..825c8637b22e8 --- /dev/null +++ b/lib/ui/annotations.dart @@ -0,0 +1,46 @@ +// 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. + +// TODO(dnfield): Remove unused_import ignores when https://github.com/dart-lang/sdk/issues/35164 is resolved. + +// @dart = 2.6 +part of dart.ui; + +// TODO(dnfield): Update this if/when we default this to on in the tool, +// see: https://github.com/flutter/flutter/issues/52759 +/// Annotation used by Flutter's Dart compiler to indicate that an +/// [Object.toString] override should not be replaced with a supercall. +/// +/// Since `dart:ui` and `package:flutter` override `toString` purely for +/// debugging purposes, the frontend compiler is instructed to replace all +/// `toString` bodies with `return super.toString()` during compilation. This +/// significantly reduces release code size, and would make it impossible to +/// implement a meaningful override of `toString` for release mode without +/// disabling the feature and losing the size savings. If a package uses this +/// feature and has some unavoidable need to keep the `toString` implementation +/// for a specific class, applying this annotation will direct the compiler +/// to leave the method body as-is. +/// +/// For example, in the following class the `toString` method will remain as +/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` +/// option would otherwise apply and replace it with `return super.toString()`. +/// +/// ```dart +/// class MyStringBuffer { +/// StringBuffer _buffer = StringBuffer(); +/// +/// // ... +/// +/// @keepToString +/// @override +/// String toString() { +/// return _buffer.toString(); +/// } +/// } +/// ``` +const _KeepToString keepToString = _KeepToString(); + +class _KeepToString { + const _KeepToString(); +} diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index 715ba20023278..d1f43ee983489 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -645,8 +645,8 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// controls where the statistics are displayed. /// /// enabledOptions is a bit field with the following bits defined: - /// - 0x01: displayRasterizerStatistics - show GPU thread frame time - /// - 0x02: visualizeRasterizerStatistics - graph GPU thread frame times + /// - 0x01: displayRasterizerStatistics - show raster thread frame time + /// - 0x02: visualizeRasterizerStatistics - graph raster thread frame times /// - 0x04: displayEngineStatistics - show UI thread frame time /// - 0x08: visualizeEngineStatistics - graph UI thread frame times /// Set enabledOptions to 0x0F to enable all the currently defined features. @@ -654,7 +654,7 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// The "UI thread" is the thread that includes all the execution of /// the main Dart isolate (the isolate that can call /// [Window.render]). The UI thread frame time is the total time - /// spent executing the [Window.onBeginFrame] callback. The "GPU + /// spent executing the [Window.onBeginFrame] callback. The "raster /// thread" is the thread (running on the CPU) that subsequently /// processes the [Scene] provided by the Dart code to turn it into /// GPU commands and send it to the GPU. diff --git a/lib/ui/compositing/scene_host.cc b/lib/ui/compositing/scene_host.cc index 7acf29ff6ec91..8eb0ff53c6bb1 100644 --- a/lib/ui/compositing/scene_host.cc +++ b/lib/ui/compositing/scene_host.cc @@ -162,8 +162,8 @@ SceneHost::SceneHost(fml::RefPtr viewHolderToken, Dart_Handle viewConnectedCallback, Dart_Handle viewDisconnectedCallback, Dart_Handle viewStateChangedCallback) - : gpu_task_runner_( - UIDartState::Current()->GetTaskRunners().GetGPUTaskRunner()), + : raster_task_runner_( + UIDartState::Current()->GetTaskRunners().GetRasterTaskRunner()), koid_(GetKoid(viewHolderToken->handle())) { auto dart_state = UIDartState::Current(); isolate_service_id_ = Dart_IsolateServiceId(Dart_CurrentIsolate()); @@ -180,7 +180,7 @@ SceneHost::SceneHost(fml::RefPtr viewHolderToken, } // This callback will be posted as a task when the |scenic::ViewHolder| - // resource is created and given an id by the GPU thread. + // resource is created and given an id by the raster thread. auto bind_callback = [scene_host = this, isolate_service_id = isolate_service_id_](scenic::ResourceId id) { @@ -188,9 +188,9 @@ SceneHost::SceneHost(fml::RefPtr viewHolderToken, scene_host_bindings.emplace(std::make_pair(key, scene_host)); }; - // Pass the raw handle to the GPU thead; destroying a |zircon::dart::Handle| - // on that thread can cause a race condition. - gpu_task_runner_->PostTask( + // Pass the raw handle to the raster thread; destroying a + // |zircon::dart::Handle| on that thread can cause a race condition. + raster_task_runner_->PostTask( [id = koid_, ui_task_runner = UIDartState::Current()->GetTaskRunners().GetUITaskRunner(), @@ -205,7 +205,7 @@ SceneHost::SceneHost(fml::RefPtr viewHolderToken, SceneHost::~SceneHost() { scene_host_bindings.erase(SceneHostBindingKey(koid_, isolate_service_id_)); - gpu_task_runner_->PostTask( + raster_task_runner_->PostTask( [id = koid_]() { flutter::ViewHolder::Destroy(id); }); } @@ -220,8 +220,9 @@ void SceneHost::setProperties(double width, double insetBottom, double insetLeft, bool focusable) { - gpu_task_runner_->PostTask([id = koid_, width, height, insetTop, insetRight, - insetBottom, insetLeft, focusable]() { + raster_task_runner_->PostTask([id = koid_, width, height, insetTop, + insetRight, insetBottom, insetLeft, + focusable]() { auto* view_holder = flutter::ViewHolder::FromId(id); FML_DCHECK(view_holder); diff --git a/lib/ui/compositing/scene_host.h b/lib/ui/compositing/scene_host.h index bf501a128ea2f..80f53866fe8a4 100644 --- a/lib/ui/compositing/scene_host.h +++ b/lib/ui/compositing/scene_host.h @@ -53,7 +53,7 @@ class SceneHost : public RefCountedDartWrappable { Dart_Handle viewDisconnectedCallback, Dart_Handle viewStateChangedCallback); - fml::RefPtr gpu_task_runner_; + fml::RefPtr raster_task_runner_; tonic::DartPersistentValue view_connected_callback_; tonic::DartPersistentValue view_disconnected_callback_; tonic::DartPersistentValue view_state_changed_callback_; diff --git a/lib/ui/dart_ui.gni b/lib/ui/dart_ui.gni index 3eef51f003e4a..28cb82e9cd125 100644 --- a/lib/ui/dart_ui.gni +++ b/lib/ui/dart_ui.gni @@ -3,6 +3,7 @@ # found in the LICENSE file. dart_ui_files = [ + "//flutter/lib/ui/annotations.dart", "//flutter/lib/ui/channel_buffers.dart", "//flutter/lib/ui/compositing.dart", "//flutter/lib/ui/geometry.dart", diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index d3f68a9b18537..571c3a18d8f9d 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -146,7 +146,7 @@ void _updateAccessibilityFeatures(int values) { if (newFeatures == window._accessibilityFeatures) return; window._accessibilityFeatures = newFeatures; - _invoke(window.onAccessibilityFeaturesChanged, window._onAccessibilityFlagsChangedZone); + _invoke(window.onAccessibilityFeaturesChanged, window._onAccessibilityFeaturesChangedZone); } @pragma('vm:entry-point') @@ -230,7 +230,7 @@ void _runMainZoned(Function startMainIsolateFunction, Function userMainFunction, List args) { startMainIsolateFunction((){ - runZoned(() { + runZonedGuarded(() { if (userMainFunction is _BinaryFunction) { // This seems to be undocumented but supported by the command line VM. // Let's do the same in case old entry-points are ported to Flutter. @@ -240,7 +240,7 @@ void _runMainZoned(Function startMainIsolateFunction, } else { userMainFunction(); } - }, onError: (Object error, StackTrace stackTrace) { + }, (Object error, StackTrace stackTrace) { _reportUnhandledException(error.toString(), stackTrace.toString()); }); }, null); @@ -276,22 +276,7 @@ void _invoke1(void callback(A a), Zone zone, A arg) { } } -/// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2]. -// ignore: unused_element -void _invoke2(void callback(A1 a1, A2 a2), Zone zone, A1 arg1, A2 arg2) { - if (callback == null) - return; - - assert(zone != null); - - if (identical(zone, Zone.current)) { - callback(arg1, arg2); - } else { - zone.runBinaryGuarded(callback, arg1, arg2); - } -} - -/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2] and [arg3]. +/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3]. void _invoke3(void callback(A1 a1, A2 a2, A3 a3), Zone zone, A1 arg1, A2 arg2, A3 arg3) { if (callback == null) return; diff --git a/lib/ui/painting/image_decoder_unittests.cc b/lib/ui/painting/image_decoder_unittests.cc index 94c008e2959d2..d90378dd625fe 100644 --- a/lib/ui/painting/image_decoder_unittests.cc +++ b/lib/ui/painting/image_decoder_unittests.cc @@ -125,7 +125,7 @@ TEST_F(ImageDecoderFixtureTest, CanCreateImageDecoder) { auto thread_task_runner = CreateNewThread(); TaskRunners runners(GetCurrentTestName(), // label thread_task_runner, // platform - thread_task_runner, // gpu + thread_task_runner, // raster thread_task_runner, // ui thread_task_runner // io @@ -146,7 +146,7 @@ TEST_F(ImageDecoderFixtureTest, InvalidImageResultsError) { auto thread_task_runner = CreateNewThread(); TaskRunners runners(GetCurrentTestName(), // label thread_task_runner, // platform - thread_task_runner, // gpu + thread_task_runner, // raster thread_task_runner, // ui thread_task_runner // io ); @@ -176,7 +176,7 @@ TEST_F(ImageDecoderFixtureTest, ValidImageResultsInSuccess) { auto loop = fml::ConcurrentMessageLoop::Create(); TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); @@ -223,7 +223,7 @@ TEST_F(ImageDecoderFixtureTest, ExifDataIsRespectedOnDecode) { auto loop = fml::ConcurrentMessageLoop::Create(); TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); @@ -275,7 +275,7 @@ TEST_F(ImageDecoderFixtureTest, CanDecodeWithoutAGPUContext) { auto loop = fml::ConcurrentMessageLoop::Create(); TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); @@ -331,7 +331,7 @@ TEST_F(ImageDecoderFixtureTest, CanDecodeWithResizes) { auto loop = fml::ConcurrentMessageLoop::Create(); TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); @@ -430,7 +430,7 @@ TEST_F(ImageDecoderFixtureTest, CanResizeWithoutDecode) { auto loop = fml::ConcurrentMessageLoop::Create(); TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); @@ -590,7 +590,7 @@ TEST_F(ImageDecoderFixtureTest, TaskRunners runners(GetCurrentTestName(), // label CreateNewThread("platform"), // platform - CreateNewThread("gpu"), // gpu + CreateNewThread("raster"), // raster CreateNewThread("ui"), // ui CreateNewThread("io") // io ); diff --git a/lib/ui/painting/image_encoding.cc b/lib/ui/painting/image_encoding.cc index cef92a7e2451a..c82f623c00af1 100644 --- a/lib/ui/painting/image_encoding.cc +++ b/lib/ui/painting/image_encoding.cc @@ -83,7 +83,7 @@ sk_sp ConvertToRasterUsingResourceContext( void ConvertImageToRaster(sk_sp image, std::function)> encode_task, - fml::RefPtr gpu_task_runner, + fml::RefPtr raster_task_runner, fml::RefPtr io_task_runner, GrContext* resource_context, fml::WeakPtr snapshot_delegate) { @@ -116,11 +116,11 @@ void ConvertImageToRaster(sk_sp image, } // Cross-context images do not support makeRasterImage. Convert these images - // by drawing them into a surface. This must be done on the GPU thread - // to prevent concurrent usage of the image on both the IO and GPU threads. - gpu_task_runner->PostTask([image, encode_task = std::move(encode_task), - resource_context, snapshot_delegate, - io_task_runner]() { + // by drawing them into a surface. This must be done on the raster thread + // to prevent concurrent usage of the image on both the IO and raster threads. + raster_task_runner->PostTask([image, encode_task = std::move(encode_task), + resource_context, snapshot_delegate, + io_task_runner]() { sk_sp raster_image = snapshot_delegate->ConvertToRasterImage(image); @@ -210,7 +210,7 @@ void EncodeImageAndInvokeDataCallback( std::unique_ptr callback, ImageByteFormat format, fml::RefPtr ui_task_runner, - fml::RefPtr gpu_task_runner, + fml::RefPtr raster_task_runner, fml::RefPtr io_task_runner, GrContext* resource_context, fml::WeakPtr snapshot_delegate) { @@ -227,7 +227,7 @@ void EncodeImageAndInvokeDataCallback( encoded = std::move(encoded)] { callback_task(encoded); }); }; - ConvertImageToRaster(std::move(image), encode_task, gpu_task_runner, + ConvertImageToRaster(std::move(image), encode_task, raster_task_runner, io_task_runner, resource_context, snapshot_delegate); } @@ -252,14 +252,14 @@ Dart_Handle EncodeImage(CanvasImage* canvas_image, task_runners.GetIOTaskRunner()->PostTask(fml::MakeCopyable( [callback = std::move(callback), image = canvas_image->image(), image_format, ui_task_runner = task_runners.GetUITaskRunner(), - gpu_task_runner = task_runners.GetGPUTaskRunner(), + raster_task_runner = task_runners.GetRasterTaskRunner(), io_task_runner = task_runners.GetIOTaskRunner(), io_manager = UIDartState::Current()->GetIOManager(), snapshot_delegate = UIDartState::Current()->GetSnapshotDelegate()]() mutable { EncodeImageAndInvokeDataCallback( std::move(image), std::move(callback), image_format, - std::move(ui_task_runner), std::move(gpu_task_runner), + std::move(ui_task_runner), std::move(raster_task_runner), std::move(io_task_runner), io_manager->GetResourceContext().get(), std::move(snapshot_delegate)); })); diff --git a/lib/ui/painting/multi_frame_codec.cc b/lib/ui/painting/multi_frame_codec.cc index ae2b4bdb83f02..6c3763b1ab529 100644 --- a/lib/ui/painting/multi_frame_codec.cc +++ b/lib/ui/painting/multi_frame_codec.cc @@ -124,7 +124,7 @@ sk_sp MultiFrameCodec::State::GetNextFrameImage( return SkImage::MakeCrossContextFromPixmap(resourceContext.get(), pixmap, true); } else { - // Defer decoding until time of draw later on the GPU thread. Can happen + // Defer decoding until time of draw later on the raster thread. Can happen // when GL operations are currently forbidden such as in the background // on iOS. return SkImage::MakeFromBitmap(bitmap); diff --git a/lib/ui/painting/picture.cc b/lib/ui/painting/picture.cc index 8527b83ec139b..511e369d6afdc 100644 --- a/lib/ui/painting/picture.cc +++ b/lib/ui/painting/picture.cc @@ -79,7 +79,7 @@ Dart_Handle Picture::RasterizeToImage(sk_sp picture, new tonic::DartPersistentValue(dart_state, raw_image_callback); auto unref_queue = dart_state->GetSkiaUnrefQueue(); auto ui_task_runner = dart_state->GetTaskRunners().GetUITaskRunner(); - auto gpu_task_runner = dart_state->GetTaskRunners().GetGPUTaskRunner(); + auto raster_task_runner = dart_state->GetTaskRunners().GetRasterTaskRunner(); auto snapshot_delegate = dart_state->GetSnapshotDelegate(); // We can't create an image on this task runner because we don't have a @@ -115,9 +115,9 @@ Dart_Handle Picture::RasterizeToImage(sk_sp picture, delete image_callback; }); - // Kick things off on the GPU. + // Kick things off on the raster rask runner. fml::TaskRunner::RunNowOrPostTask( - gpu_task_runner, + raster_task_runner, [ui_task_runner, snapshot_delegate, picture, picture_bounds, ui_task] { sk_sp raster_image = snapshot_delegate->MakeRasterSnapshot(picture, picture_bounds); diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart index 2f8692c803d15..2d46db0404f6f 100644 --- a/lib/ui/ui.dart +++ b/lib/ui/ui.dart @@ -23,6 +23,7 @@ import 'dart:math' as math; import 'dart:nativewrappers'; import 'dart:typed_data'; +part 'annotations.dart'; part 'channel_buffers.dart'; part 'compositing.dart'; part 'geometry.dart'; diff --git a/lib/ui/window.dart b/lib/ui/window.dart index c830b6f4c215c..d03ece6328364 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -57,12 +57,12 @@ enum FramePhase { /// See also [FrameTiming.buildDuration]. buildFinish, - /// When the GPU thread starts rasterizing a frame. + /// When the raster thread starts rasterizing a frame. /// /// See also [FrameTiming.rasterDuration]. rasterStart, - /// When the GPU thread finishes rasterizing a frame. + /// When the raster thread finishes rasterizing a frame. /// /// See also [FrameTiming.rasterDuration]. rasterFinish, @@ -115,7 +115,7 @@ class FrameTiming { /// {@endtemplate} Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); - /// The duration to rasterize the frame on the GPU thread. + /// The duration to rasterize the frame on the raster thread. /// /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} /// {@macro dart.ui.FrameTiming.fps_milliseconds} @@ -475,12 +475,14 @@ class Locale { return true; return other is Locale && other.languageCode == languageCode - && other.scriptCode == scriptCode - && other.countryCode == countryCode; + && other.scriptCode == scriptCode // scriptCode cannot be '' + && (other.countryCode == countryCode // Treat '' as equal to null. + || other.countryCode != null && other.countryCode.isEmpty && countryCode == null + || countryCode != null && countryCode.isEmpty && other.countryCode == null); } @override - int get hashCode => hashValues(languageCode, scriptCode, countryCode); + int get hashCode => hashValues(languageCode, scriptCode, countryCode == '' ? null : countryCode); static Locale _cachedLocale; static String _cachedLocaleString; @@ -490,6 +492,7 @@ class Locale { /// This identifier happens to be a valid Unicode Locale Identifier using /// underscores as separator, however it is intended to be used for debugging /// purposes only. For parseable results, use [toLanguageTag] instead. + @keepToString @override String toString() { if (!identical(_cachedLocale, this)) { @@ -508,9 +511,9 @@ class Locale { String _rawToString(String separator) { final StringBuffer out = StringBuffer(languageCode); - if (scriptCode != null) + if (scriptCode != null && scriptCode.isNotEmpty) out.write('$separator$scriptCode'); - if (_countryCode != null) + if (_countryCode != null && _countryCode.isNotEmpty) out.write('$separator$countryCode'); return out.toString(); } @@ -1098,10 +1101,10 @@ class Window { /// callback was set. VoidCallback get onAccessibilityFeaturesChanged => _onAccessibilityFeaturesChanged; VoidCallback _onAccessibilityFeaturesChanged; - Zone _onAccessibilityFlagsChangedZone; + Zone _onAccessibilityFeaturesChangedZone; set onAccessibilityFeaturesChanged(VoidCallback callback) { _onAccessibilityFeaturesChanged = callback; - _onAccessibilityFlagsChangedZone = Zone.current; + _onAccessibilityFeaturesChangedZone = Zone.current; } /// Change the retained semantics data about this window. diff --git a/lib/ui/window/pointer_data_packet_converter.cc b/lib/ui/window/pointer_data_packet_converter.cc index c7f643f25417d..b945e7b1463c4 100644 --- a/lib/ui/window/pointer_data_packet_converter.cc +++ b/lib/ui/window/pointer_data_packet_converter.cc @@ -169,11 +169,9 @@ void PointerDataPacketConverter::ConvertPointerData( PointerState state = iter->second; FML_DCHECK(state.isDown); - if (LocationNeedsUpdate(pointer_data, state)) { - UpdatePointerIdentifier(pointer_data, state, false); - UpdateDeltaAndState(pointer_data, state); - converted_pointers.push_back(pointer_data); - } + UpdatePointerIdentifier(pointer_data, state, false); + UpdateDeltaAndState(pointer_data, state); + converted_pointers.push_back(pointer_data); break; } case PointerData::Change::kUp: { diff --git a/lib/ui/window/pointer_data_packet_converter_unittests.cc b/lib/ui/window/pointer_data_packet_converter_unittests.cc index d18b7f3e474ed..926637244f8b6 100644 --- a/lib/ui/window/pointer_data_packet_converter_unittests.cc +++ b/lib/ui/window/pointer_data_packet_converter_unittests.cc @@ -244,6 +244,43 @@ TEST(PointerDataPacketConverterTest, CanUpdatePointerIdentifier) { ASSERT_EQ(result[6].synthesized, 0); } +TEST(PointerDataPacketConverterTest, AlwaysForwardMoveEvent) { + PointerDataPacketConverter converter; + auto packet = std::make_unique(4); + PointerData data; + CreateSimulatedPointerData(data, PointerData::Change::kAdd, 0, 0.0, 0.0); + packet->SetPointerData(0, data); + CreateSimulatedPointerData(data, PointerData::Change::kDown, 0, 0.0, 0.0); + packet->SetPointerData(1, data); + // Creates a move event without a location change. + CreateSimulatedPointerData(data, PointerData::Change::kMove, 0, 0.0, 0.0); + packet->SetPointerData(2, data); + CreateSimulatedPointerData(data, PointerData::Change::kUp, 0, 0.0, 0.0); + packet->SetPointerData(3, data); + + auto converted_packet = converter.Convert(std::move(packet)); + + std::vector result; + UnpackPointerPacket(result, std::move(converted_packet)); + + ASSERT_EQ(result.size(), (size_t)4); + ASSERT_EQ(result[0].change, PointerData::Change::kAdd); + ASSERT_EQ(result[0].synthesized, 0); + + ASSERT_EQ(result[1].change, PointerData::Change::kDown); + ASSERT_EQ(result[1].pointer_identifier, 1); + ASSERT_EQ(result[1].synthesized, 0); + + // Does not filter out the move event. + ASSERT_EQ(result[2].change, PointerData::Change::kMove); + ASSERT_EQ(result[2].pointer_identifier, 1); + ASSERT_EQ(result[2].synthesized, 0); + + ASSERT_EQ(result[3].change, PointerData::Change::kUp); + ASSERT_EQ(result[3].pointer_identifier, 1); + ASSERT_EQ(result[3].synthesized, 0); +} + TEST(PointerDataPacketConverterTest, CanWorkWithDifferentDevices) { PointerDataPacketConverter converter; auto packet = std::make_unique(12); diff --git a/lib/web_ui/.gitignore b/lib/web_ui/.gitignore index 567609b1234a9..afc0a9b3df77e 100644 --- a/lib/web_ui/.gitignore +++ b/lib/web_ui/.gitignore @@ -1 +1,2 @@ build/ +chromedriver/ diff --git a/lib/web_ui/analysis_options.yaml b/lib/web_ui/analysis_options.yaml index 6892f49dc726b..5b40ea7c85c1e 100644 --- a/lib/web_ui/analysis_options.yaml +++ b/lib/web_ui/analysis_options.yaml @@ -1 +1,87 @@ -# Intentionally kept empty. \ No newline at end of file +# This is copy of the root analysis_options.yaml. As we clean up the Web code, +# we'll be uncommenting rules and gradually fix the code. When all rules are +# uncommented, we'll delete this file and simply inherit the root options. + +analyzer: + strong-mode: + # TODO(uncomment) implicit-casts: false + implicit-dynamic: false + errors: + missing_required_param: warning + missing_return: warning + native_function_body_in_non_sdk_code: ignore + todo: ignore + +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + # TODO(uncomment) - always_specify_types + # TODO(uncomment) - annotate_overrides + # TODO(uncomment) - avoid_classes_with_only_static_members + # TODO(uncomment) - avoid_empty_else + # TODO(uncomment) - avoid_function_literals_in_foreach_calls + # TODO(uncomment) - avoid_init_to_null + # TODO(uncomment) - avoid_null_checks_in_equality_operators + # TODO(uncomment) - avoid_relative_lib_imports + # TODO(uncomment) - avoid_renaming_method_parameters + # TODO(uncomment) - avoid_return_types_on_setters + # TODO(uncomment) - avoid_slow_async_io + # TODO(uncomment) - await_only_futures + # TODO(uncomment) - camel_case_types + # TODO(uncomment) - cancel_subscriptions + # TODO(uncomment) - control_flow_in_finally + # TODO(uncomment) - directives_ordering + # TODO(uncomment) - empty_catches + # TODO(uncomment) - empty_constructor_bodies + # TODO(uncomment) - empty_statements + # TODO(uncomment) - hash_and_equals + # TODO(uncomment) - implementation_imports + # TODO(uncomment) - iterable_contains_unrelated_type + # TODO(uncomment) - library_names + # TODO(uncomment) - library_prefixes + # TODO(uncomment) - list_remove_unrelated_type + # TODO(uncomment) - no_adjacent_strings_in_list + # TODO(uncomment) - no_duplicate_case_values + # TODO(uncomment) - non_constant_identifier_names + # TODO(uncomment) - overridden_fields + # TODO(uncomment) - package_api_docs + # TODO(uncomment) - package_names + # TODO(uncomment) - package_prefixed_library_names + # TODO(uncomment) - prefer_adjacent_string_concatenation + # TODO(uncomment) - prefer_asserts_in_initializer_lists + # TODO(uncomment) - prefer_collection_literals + # TODO(uncomment) - prefer_conditional_assignment + # TODO(uncomment) - prefer_const_constructors + # TODO(uncomment) - prefer_const_constructors_in_immutables + # TODO(uncomment) - prefer_const_declarations + # TODO(uncomment) - prefer_const_literals_to_create_immutables + # TODO(uncomment) - prefer_contains + # TODO(uncomment) - prefer_equal_for_default_values + # TODO(uncomment) - prefer_final_locals + # TODO(uncomment) - prefer_foreach + # TODO(uncomment) - prefer_generic_function_type_aliases + # TODO(uncomment) - prefer_initializing_formals + # TODO(uncomment) - prefer_is_empty + # TODO(uncomment) - prefer_is_not_empty + # TODO(uncomment) - prefer_single_quotes + # TODO(uncomment) - prefer_typing_uninitialized_variables + # TODO(uncomment) - public_member_api_docs + # TODO(uncomment) - recursive_getters + # TODO(uncomment) - slash_for_doc_comments + # TODO(uncomment) - sort_unnamed_constructors_first + # TODO(uncomment) - test_types_in_equals + # TODO(uncomment) - throw_in_finally + # TODO(uncomment) - type_init_formals + # TODO(uncomment) - unnecessary_brace_in_string_interps + # TODO(uncomment) - unnecessary_const + # TODO(uncomment) - unnecessary_getters_setters + # TODO(uncomment) - unnecessary_new + # TODO(uncomment) - unnecessary_null_aware_assignments + # TODO(uncomment) - unnecessary_null_in_if_null_operators + # TODO(uncomment) - unnecessary_overrides + # TODO(uncomment) - unnecessary_parenthesis + # TODO(uncomment) - unnecessary_this + # TODO(uncomment) - unrelated_type_equality_checks + # TODO(uncomment) - use_rethrow_when_possible + # TODO(uncomment) - valid_regexps diff --git a/lib/web_ui/dev/README.md b/lib/web_ui/dev/README.md index 5e5f5d45c0832..dba654e49c54e 100644 --- a/lib/web_ui/dev/README.md +++ b/lib/web_ui/dev/README.md @@ -31,12 +31,30 @@ felt build [-w] -j 100 If you are a Google employee, you can use an internal instance of Goma to parallelize your builds. Because Goma compiles code on remote servers, this option is effective even on low-powered laptops. ## Running web engine tests -To run all tests on Chrome: +To run all tests on Chrome. This will run both integration tests and the unit tests: ``` felt test ``` +To run unit tests only: + +``` +felt test --unit-tests-only +``` + +To run integration tests only. For now these tests are only available on Chrome Desktop browsers. These tests will fetch the flutter repository for using `flutter drive` and `flutter pub get` commands. The repository will be synced to the youngest commit older than the engine commit. + +``` +felt test --integration-tests-only +``` + +To skip cloning the flutter repository use the following flag. This flag can save internet bandwidth. However use with caution. Note the tests results will not be consistent with CIs when this flag is set. flutter command should be set in the PATH for this flag to be useful. This flag can also be used to test local Flutter changes. + +``` +felt test --integration-tests-only --use-system-flutter +``` + To run tests on Firefox (this will work only on a Linux device): ``` @@ -55,7 +73,7 @@ To run tests on Safari use the following command. It works on MacOS devices and felt test --browser=safari ``` -To run tests on Windows Edge use the following command. It works on Windows devices and it uses the Edge installed on the OS. +To run tests on Windows Edge use the following command. It works on Windows devices and it uses the Edge installed on the OS. ``` felt_windows.bat test --browser=edge diff --git a/lib/web_ui/dev/analysis_options.yaml b/lib/web_ui/dev/analysis_options.yaml new file mode 100644 index 0000000000000..ac1dc7ce40a6a --- /dev/null +++ b/lib/web_ui/dev/analysis_options.yaml @@ -0,0 +1,7 @@ +# This is a temporary file used to clean up dev/ before other directories +include: ../analysis_options.yaml + +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false diff --git a/lib/web_ui/dev/browser.dart b/lib/web_ui/dev/browser.dart index 8c4c736e99aab..71d7de547a603 100644 --- a/lib/web_ui/dev/browser.dart +++ b/lib/web_ui/dev/browser.dart @@ -51,8 +51,8 @@ abstract class Browser { /// /// If there's a problem starting or running the browser, this will complete /// with an error. - Future get onExit => _onExitCompleter.future; - final _onExitCompleter = Completer(); + Future get onExit => _onExitCompleter.future; + final _onExitCompleter = Completer(); /// Standard IO streams for the underlying browser process. final _ioSubscriptions = []; @@ -72,7 +72,7 @@ abstract class Browser { _processCompleter.complete(process); var output = Uint8Buffer(); - drainOutput(Stream> stream) { + void drainOutput(Stream> stream) { try { _ioSubscriptions .add(stream.listen(output.addAll, cancelOnError: true)); @@ -96,7 +96,7 @@ abstract class Browser { // resolve the ambiguity is to wait a brief amount of time and see if this // browser is actually closed. if (!_closed && exitCode < 0) { - await Future.delayed(Duration(milliseconds: 200)); + await Future.delayed(Duration(milliseconds: 200)); } if (!_closed && exitCode != 0) { @@ -110,15 +110,21 @@ abstract class Browser { } _onExitCompleter.complete(); - }, onError: (error, StackTrace stackTrace) { + }, onError: (dynamic error, StackTrace stackTrace) { // Ignore any errors after the browser has been closed. - if (_closed) return; + if (_closed) { + return; + } // Make sure the process dies even if the error wasn't fatal. _process.then((process) => process.kill()); - if (stackTrace == null) stackTrace = Trace.current(); - if (_onExitCompleter.isCompleted) return; + if (stackTrace == null) { + stackTrace = Trace.current(); + } + if (_onExitCompleter.isCompleted) { + return; + } _onExitCompleter.completeError( Exception('Failed to run $name: ${getErrorMessage(error)}.'), stackTrace); @@ -142,6 +148,6 @@ abstract class Browser { (await _process).kill(); // Swallow exceptions. The user should explicitly use [onExit] for these. - return onExit.catchError((_) {}); + return onExit.catchError((dynamic _) {}); } } diff --git a/lib/web_ui/dev/browser_lock.yaml b/lib/web_ui/dev/browser_lock.yaml index 970cf1feb5596..2bede5bcd221a 100644 --- a/lib/web_ui/dev/browser_lock.yaml +++ b/lib/web_ui/dev/browser_lock.yaml @@ -1,7 +1,7 @@ chrome: # It seems Chrome can't always release from the same build for all operating # systems, so we specify per-OS build number. - Linux: 735129 + Linux: 753189 Mac: 735116 Win: 735105 firefox: diff --git a/lib/web_ui/dev/build.dart b/lib/web_ui/dev/build.dart index a6c70405964bb..7e31a3fdb2399 100644 --- a/lib/web_ui/dev/build.dart +++ b/lib/web_ui/dev/build.dart @@ -13,7 +13,7 @@ import 'package:watcher/watcher.dart'; import 'environment.dart'; import 'utils.dart'; -class BuildCommand extends Command { +class BuildCommand extends Command with ArgUtils { BuildCommand() { argParser ..addFlag( @@ -35,15 +35,9 @@ class BuildCommand extends Command { @override String get description => 'Build the Flutter web engine.'; - bool get isWatchMode => argResults['watch']; + bool get isWatchMode => boolArg('watch'); - int getNinjaJobCount() { - final String ninjaJobsArg = argResults['ninja-jobs']; - if (ninjaJobsArg != null) { - return int.tryParse(ninjaJobsArg); - } - return null; - } + int getNinjaJobCount() => intArg('ninja-jobs'); @override FutureOr run() async { diff --git a/lib/web_ui/dev/chrome_installer.dart b/lib/web_ui/dev/chrome_installer.dart index e761446545e29..48e10def23fa8 100644 --- a/lib/web_ui/dev/chrome_installer.dart +++ b/lib/web_ui/dev/chrome_installer.dart @@ -13,6 +13,7 @@ import 'package:yaml/yaml.dart'; import 'common.dart'; import 'environment.dart'; +import 'exceptions.dart'; class ChromeArgParser extends BrowserArgParser { static final ChromeArgParser _singletonInstance = ChromeArgParser._(); @@ -45,7 +46,7 @@ class ChromeArgParser extends BrowserArgParser { @override void parseOptions(ArgResults argResults) { - _version = argResults['chrome-version']; + _version = argResults['chrome-version'] as String; } @override @@ -108,7 +109,7 @@ Future _findSystemChromeExecutable() async { 'Failed to locate system Chrome installation.'); } - return which.stdout; + return which.stdout as String; } /// Manages the installation of a particular [version] of Chrome. @@ -227,3 +228,64 @@ Future fetchLatestChromeVersion() async { client.close(); } } + +/// Get the Chrome Driver version for the system Chrome. +// TODO(nurhan): https://github.com/flutter/flutter/issues/53179 +Future queryChromeDriverVersion() async { + final int chromeVersion = await _querySystemChromeMajorVersion(); + final io.File lockFile = io.File( + path.join(environment.webUiRootDir.path, 'dev', 'driver_version.yaml')); + YamlMap _configuration = loadYaml(lockFile.readAsStringSync()) as YamlMap; + final String chromeDriverVersion = + _configuration['chrome'][chromeVersion] as String; + return chromeDriverVersion; +} + +Future _querySystemChromeMajorVersion() async { + String chromeExecutable = ''; + if (io.Platform.isLinux) { + chromeExecutable = 'google-chrome'; + } else if (io.Platform.isMacOS) { + chromeExecutable = await _findChromeExecutableOnMac(); + } else { + throw UnimplementedError('Web installers only work on Linux and Mac.'); + } + + final io.ProcessResult versionResult = + await io.Process.run('$chromeExecutable', ['--version']); + + if (versionResult.exitCode != 0) { + throw Exception('Failed to locate system Chrome.'); + } + // The output looks like: Google Chrome 79.0.3945.36. + final String output = versionResult.stdout as String; + + print('INFO: chrome version in use $output'); + + // Version number such as 79.0.3945.36. + try { + final String versionAsString = output.trim().split(' ').last; + final String majorVersion = versionAsString.split('.')[0]; + return int.parse(majorVersion); + } catch (e) { + throw Exception( + 'Was expecting a version of the form Google Chrome 79.0.3945.36., ' + 'received $output'); + } +} + +/// Find Google Chrome App on Mac. +Future _findChromeExecutableOnMac() async { + io.Directory chromeDirectory = io.Directory('/Applications') + .listSync() + .whereType() + .firstWhere( + (d) => path.basename(d.path).endsWith('Chrome.app'), + orElse: () => throw Exception('Failed to locate system Chrome'), + ); + + final io.File chromeExecutableDir = io.File( + path.join(chromeDirectory.path, 'Contents', 'MacOS', 'Google Chrome')); + + return chromeExecutableDir.path; +} diff --git a/lib/web_ui/dev/clean.dart b/lib/web_ui/dev/clean.dart index 712e4c28e2a3e..b3d065fc66534 100644 --- a/lib/web_ui/dev/clean.dart +++ b/lib/web_ui/dev/clean.dart @@ -10,10 +10,16 @@ import 'package:args/command_runner.dart'; import 'package:path/path.dart' as path; import 'environment.dart'; +import 'utils.dart'; -class CleanCommand extends Command { +class CleanCommand extends Command with ArgUtils { CleanCommand() { argParser + ..addFlag( + 'flutter', + defaultsTo: true, + help: 'Cleans up the .dart_tool directory under engine/src/flutter.', + ) ..addFlag( 'ninja', defaultsTo: false, @@ -24,7 +30,9 @@ class CleanCommand extends Command { @override String get name => 'clean'; - bool get _alsoCleanNinja => argResults['ninja']; + bool get _alsoCleanNinja => boolArg('ninja'); + + bool get _alsoCleanFlutterRepo => boolArg('flutter'); @override String get description => 'Deletes build caches and artifacts.'; @@ -47,6 +55,8 @@ class CleanCommand extends Command { ...fontFiles, if (_alsoCleanNinja) environment.outDir, + if(_alsoCleanFlutterRepo) + environment.engineDartToolDir, ]; await Future.wait( diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart index 9fa8d126c86ae..588fe67eedf6f 100644 --- a/lib/web_ui/dev/common.dart +++ b/lib/web_ui/dev/common.dart @@ -18,15 +18,6 @@ const int kMaxScreenshotWidth = 1024; const int kMaxScreenshotHeight = 1024; const double kMaxDiffRateFailure = 0.28 / 100; // 0.28% -class BrowserInstallerException implements Exception { - BrowserInstallerException(this.message); - - final String message; - - @override - String toString() => message; -} - abstract class PlatformBinding { static PlatformBinding get instance { if (_instance == null) { @@ -62,8 +53,8 @@ const String _kBaseDownloadUrl = class _WindowsBinding implements PlatformBinding { @override int getChromeBuild(YamlMap browserLock) { - final YamlMap chromeMap = browserLock['chrome']; - return chromeMap['Win']; + final YamlMap chromeMap = browserLock['chrome'] as YamlMap; + return chromeMap['Win'] as int; } @override @@ -77,11 +68,10 @@ class _WindowsBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/' - '${getFirefoxDownloadFilename(version)}'; + '${getFirefoxDownloadFilename(version)}'; @override - String getFirefoxDownloadFilename(String version) => - 'firefox-${version}.exe'; + String getFirefoxDownloadFilename(String version) => 'firefox-${version}.exe'; @override String getFirefoxExecutablePath(io.Directory versionDir) => @@ -102,8 +92,8 @@ class _WindowsBinding implements PlatformBinding { class _LinuxBinding implements PlatformBinding { @override int getChromeBuild(YamlMap browserLock) { - final YamlMap chromeMap = browserLock['chrome']; - return chromeMap['Linux']; + final YamlMap chromeMap = browserLock['chrome'] as YamlMap; + return chromeMap['Linux'] as int; } @override @@ -117,7 +107,7 @@ class _LinuxBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/' - '${getFirefoxDownloadFilename(version)}'; + '${getFirefoxDownloadFilename(version)}'; @override String getFirefoxDownloadFilename(String version) => @@ -143,8 +133,8 @@ class _LinuxBinding implements PlatformBinding { class _MacBinding implements PlatformBinding { @override int getChromeBuild(YamlMap browserLock) { - final YamlMap chromeMap = browserLock['chrome']; - return chromeMap['Mac']; + final YamlMap chromeMap = browserLock['chrome'] as YamlMap; + return chromeMap['Mac'] as int; } @override @@ -161,16 +151,15 @@ class _MacBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => - 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/' - '${getFirefoxDownloadFilename(version)}'; + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/' + '${getFirefoxDownloadFilename(version)}'; @override - String getFirefoxDownloadFilename(String version) => - 'Firefox ${version}.dmg'; + String getFirefoxDownloadFilename(String version) => 'Firefox ${version}.dmg'; @override String getFirefoxExecutablePath(io.Directory versionDir) => - path.join(versionDir.path, 'Firefox.app','Contents','MacOS', 'firefox'); + path.join(versionDir.path, 'Firefox.app', 'Contents', 'MacOS', 'firefox'); @override String getFirefoxLatestVersionUrl() => @@ -185,10 +174,10 @@ class _MacBinding implements PlatformBinding { } class BrowserInstallation { - const BrowserInstallation( - {@required this.version, - @required this.executable, - fetchLatestChromeVersion}); + const BrowserInstallation({ + @required this.version, + @required this.executable, + }); /// Browser version. final String version; @@ -223,7 +212,7 @@ class BrowserLock { BrowserLock._() { final io.File lockFile = io.File( path.join(environment.webUiRootDir.path, 'dev', 'browser_lock.yaml')); - this._configuration = loadYaml(lockFile.readAsStringSync()); + this._configuration = loadYaml(lockFile.readAsStringSync()) as YamlMap; } } @@ -242,4 +231,12 @@ class DevNull implements StringSink { void writeln([Object obj = ""]) {} } +/// Whether the felt command is running on Cirrus CI. bool get isCirrus => io.Platform.environment['CIRRUS_CI'] == 'true'; + +/// Whether the felt command is running on LUCI. +bool get isLuci => io.Platform.environment['LUCI_CONTEXT'] != null; + +/// Whether the felt command is running on one of the Continuous Integration +/// environements. +bool get isCi => isCirrus || isLuci; diff --git a/lib/web_ui/dev/driver_version.yaml b/lib/web_ui/dev/driver_version.yaml new file mode 100644 index 0000000000000..5515e1fbca7b0 --- /dev/null +++ b/lib/web_ui/dev/driver_version.yaml @@ -0,0 +1,12 @@ + +## Map for driver versions to use for each browser version. +## See: https://chromedriver.chromium.org/downloads +chrome: + 81: '81.0.4044.69' + 80: '80.0.3987.106' + 79: '79.0.3945.36' + 78: '78.0.3904.105' + 77: '77.0.3865.40' + 76: '76.0.3809.126' + 75: '75.0.3770.140' + 74: '74.0.3729.6' diff --git a/lib/web_ui/dev/edge_installation.dart b/lib/web_ui/dev/edge_installation.dart index 3190474164d00..efdf03f30b1b1 100644 --- a/lib/web_ui/dev/edge_installation.dart +++ b/lib/web_ui/dev/edge_installation.dart @@ -36,7 +36,7 @@ class EdgeArgParser extends BrowserArgParser { @override void parseOptions(ArgResults argResults) { - _version = argResults['edge-version']; + _version = argResults['edge-version'] as String; assert(_version == 'system'); } @@ -124,7 +124,7 @@ class EdgeLauncher { EdgeLauncher() : version = - BrowserLock.instance.configuration['edge']['launcher_version']; + BrowserLock.instance.configuration['edge']['launcher_version'] as String; /// Install the launcher if it does not exist in this system. void install() async { diff --git a/lib/web_ui/dev/environment.dart b/lib/web_ui/dev/environment.dart index 35c11a9e073dc..824bded595267 100644 --- a/lib/web_ui/dev/environment.dart +++ b/lib/web_ui/dev/environment.dart @@ -6,6 +6,8 @@ import 'dart:io' as io; import 'package:path/path.dart' as pathlib; +import 'exceptions.dart'; + /// Contains various environment variables, such as common file paths and command-line options. Environment get environment { _environment ??= Environment(); @@ -18,15 +20,16 @@ class Environment { factory Environment() { final io.File self = io.File.fromUri(io.Platform.script); final io.Directory engineSrcDir = self.parent.parent.parent.parent.parent; + final io.Directory engineToolsDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'tools')); final io.Directory outDir = io.Directory(pathlib.join(engineSrcDir.path, 'out')); final io.Directory hostDebugUnoptDir = io.Directory(pathlib.join(outDir.path, 'host_debug_unopt')); final io.Directory dartSdkDir = io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk')); final io.Directory webUiRootDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'lib', 'web_ui')); + final io.Directory integrationTestsDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'e2etests', 'web')); for (io.Directory expectedDirectory in [engineSrcDir, outDir, hostDebugUnoptDir, dartSdkDir, webUiRootDir]) { if (!expectedDirectory.existsSync()) { - io.stderr.writeln('$expectedDirectory does not exist.'); - io.exit(1); + throw ToolException('$expectedDirectory does not exist.'); } } @@ -34,6 +37,8 @@ class Environment { self: self, webUiRootDir: webUiRootDir, engineSrcDir: engineSrcDir, + engineToolsDir: engineToolsDir, + integrationTestsDir: integrationTestsDir, outDir: outDir, hostDebugUnoptDir: hostDebugUnoptDir, dartSdkDir: dartSdkDir, @@ -44,6 +49,8 @@ class Environment { this.self, this.webUiRootDir, this.engineSrcDir, + this.engineToolsDir, + this.integrationTestsDir, this.outDir, this.hostDebugUnoptDir, this.dartSdkDir, @@ -58,6 +65,12 @@ class Environment { /// Path to the engine's "src" directory. final io.Directory engineSrcDir; + /// Path to the engine's "tools" directory. + final io.Directory engineToolsDir; + + /// Path to the web integration tests. + final io.Directory integrationTestsDir; + /// Path to the engine's "out" directory. /// /// This is where you'll find the ninja output, such as the Dart SDK. @@ -105,6 +118,16 @@ class Environment { '.dart_tool', )); + /// Path to the ".dart_tool" directory living under `engine/src/flutter`. + /// + /// This is a designated area for tool downloads which can be used by + /// multiple platforms. For exampe: Flutter repo for e2e tests. + io.Directory get engineDartToolDir => io.Directory(pathlib.join( + engineSrcDir.path, + 'flutter', + '.dart_tool', + )); + /// Path to the "dev" directory containing engine developer tools and /// configuration files. io.Directory get webUiDevDir => io.Directory(pathlib.join( @@ -117,4 +140,23 @@ class Environment { webUiDartToolDir.path, 'goldens', )); + + /// Path to the script that clones the Flutter repo. + io.File get cloneFlutterScript => io.File(pathlib.join( + engineToolsDir.path, + 'clone_flutter.sh', + )); + + /// Path to flutter. + /// + /// For example, this can be used to run `flutter pub get`. + /// + /// Only use [cloneFlutterScript] to clone flutter to the engine build. + io.File get flutterCommand => io.File(pathlib.join( + engineDartToolDir.path, + 'flutter', + 'bin', + 'flutter', + )); + } diff --git a/lib/web_ui/dev/exceptions.dart b/lib/web_ui/dev/exceptions.dart new file mode 100644 index 0000000000000..167d1734e655f --- /dev/null +++ b/lib/web_ui/dev/exceptions.dart @@ -0,0 +1,30 @@ +// 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. + +class BrowserInstallerException implements Exception { + BrowserInstallerException(this.message); + + final String message; + + @override + String toString() => message; +} + +class DriverException implements Exception { + DriverException(this.message); + + final String message; + + @override + String toString() => message; +} + +class ToolException implements Exception { + ToolException(this.message); + + final String message; + + @override + String toString() => message; +} diff --git a/lib/web_ui/dev/felt b/lib/web_ui/dev/felt index 07a71f37e8211..56b031ce0b616 100755 --- a/lib/web_ui/dev/felt +++ b/lib/web_ui/dev/felt @@ -56,9 +56,9 @@ install_deps() { KERNEL_NAME=`uname` if [[ $KERNEL_NAME == *"Darwin"* ]] then - echo "Running on MacOS. Increase the user limits" - ulimit -n 50000 - ulimit -u 4096 + echo "Running on MacOS. Increase the user limits" + ulimit -n 50000 + ulimit -u 4096 fi if [[ "$FELT_USE_SNAPSHOT" == "false" || "$FELT_USE_SNAPSHOT" == "0" ]]; then diff --git a/lib/web_ui/dev/felt.dart b/lib/web_ui/dev/felt.dart index d192431791b39..6366b6152991a 100644 --- a/lib/web_ui/dev/felt.dart +++ b/lib/web_ui/dev/felt.dart @@ -10,7 +10,9 @@ import 'package:args/command_runner.dart'; import 'build.dart'; import 'clean.dart'; import 'licenses.dart'; +import 'exceptions.dart'; import 'test_runner.dart'; +import 'utils.dart'; CommandRunner runner = CommandRunner( 'felt', @@ -30,32 +32,45 @@ void main(List args) async { _listenToShutdownSignals(); + int exitCode = -1; try { - final bool result = await runner.run(args); + final bool result = (await runner.run(args)) as bool; if (result == false) { print('Sub-command returned false: `${args.join(' ')}`'); - io.exit(1); + exitCode = 1; } } on UsageException catch (e) { print(e); - io.exit(64); // Exit code 64 indicates a usage error. + exitCode = 64; // Exit code 64 indicates a usage error. + } on ToolException catch (e) { + io.stderr.writeln(e.message); + exitCode = 1; } catch (e) { rethrow; + } finally { + await cleanup(); + // The exit code is changed by one of the branches. + if(exitCode != -1) { + io.exit(exitCode); + } } // Sometimes the Dart VM refuses to quit. io.exit(io.exitCode); } -void _listenToShutdownSignals() { - io.ProcessSignal.sigint.watch().listen((_) { +void _listenToShutdownSignals() async { + io.ProcessSignal.sigint.watch().listen((_) async { print('Received SIGINT. Shutting down.'); + await cleanup(); io.exit(1); }); + // SIGTERM signals are not generated under Windows. // See https://docs.microsoft.com/en-us/previous-versions/xdkz3x12(v%3Dvs.140) if (!io.Platform.isWindows) { - io.ProcessSignal.sigterm.watch().listen((_) { + io.ProcessSignal.sigterm.watch().listen((_) async { + await cleanup(); print('Received SIGTERM. Shutting down.'); io.exit(1); }); diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index d3043e9f68303..e282d2a3df23f 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -12,6 +12,7 @@ import 'package:yaml/yaml.dart'; import 'common.dart'; import 'environment.dart'; +import 'exceptions.dart'; class FirefoxArgParser extends BrowserArgParser { static final FirefoxArgParser _singletonInstance = FirefoxArgParser._(); @@ -26,7 +27,7 @@ class FirefoxArgParser extends BrowserArgParser { @override void populateOptions(ArgParser argParser) { final YamlMap browserLock = BrowserLock.instance.configuration; - String firefoxVersion = browserLock['firefox']['version']; + String firefoxVersion = browserLock['firefox']['version'] as String; argParser ..addOption( @@ -42,7 +43,7 @@ class FirefoxArgParser extends BrowserArgParser { @override void parseOptions(ArgResults argResults) { - _version = argResults['firefox-version']; + _version = argResults['firefox-version'] as String; } @override @@ -260,7 +261,7 @@ class FirefoxInstaller { 'Exit code ${mountResult.exitCode}.\n${mountResult.stderr}'); } - List processOutput = mountResult.stdout.split('\n'); + List processOutput = (mountResult.stdout as String).split('\n'); String volumePath = _volumeFromMountResult(processOutput); if (volumePath == null) { throw BrowserInstallerException( @@ -313,7 +314,7 @@ Future _findSystemFirefoxExecutable() async { throw BrowserInstallerException( 'Failed to locate system Firefox installation.'); } - return which.stdout; + return which.stdout as String; } /// Fetches the latest available Firefox build version on Linux. diff --git a/lib/web_ui/dev/firefox_installer_test.dart b/lib/web_ui/dev/firefox_installer_test.dart index 2d687d7be1c6b..fc18277f53b14 100644 --- a/lib/web_ui/dev/firefox_installer_test.dart +++ b/lib/web_ui/dev/firefox_installer_test.dart @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.6 @TestOn('vm && linux') -// @dart = 2.6 import 'dart:io' as io; import 'package:path/path.dart' as path; diff --git a/lib/web_ui/dev/goldens.dart b/lib/web_ui/dev/goldens.dart index 4e61ecb805799..fef621c724385 100644 --- a/lib/web_ui/dev/goldens.dart +++ b/lib/web_ui/dev/goldens.dart @@ -72,10 +72,10 @@ class ImageDiff { /// That would be the distance between black and white. static final double _maxTheoreticalColorDistance = Color.distance( - [255, 255, 255], // white - [0, 0, 0], // black + [255, 255, 255], // white + [0, 0, 0], // black false, - ); + ).toDouble(); // If the normalized color difference of a pixel is greater than this number, // we consider it a wrong pixel. @@ -203,9 +203,9 @@ class _GoldensRepoFetcher { final io.File lockFile = io.File( path.join(environment.webUiDevDir.path, 'goldens_lock.yaml') ); - final YamlMap lock = loadYaml(lockFile.readAsStringSync()); - _repository = lock['repository']; - _revision = lock['revision']; + final YamlMap lock = loadYaml(lockFile.readAsStringSync()) as YamlMap; + _repository = lock['repository'] as String; + _revision = lock['revision'] as String; final String localRevision = await _getLocalRevision(); if (localRevision == _revision) { diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 1b73145f48a3b..084f6e21c666d 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 8f692819e8881b7d2131dbd61d965c21d5e3e345 +revision: 1839716cce5da0fd4c442395c7161f29dd983d8e diff --git a/lib/web_ui/dev/integration_tests_manager.dart b/lib/web_ui/dev/integration_tests_manager.dart new file mode 100644 index 0000000000000..ec3595dedf268 --- /dev/null +++ b/lib/web_ui/dev/integration_tests_manager.dart @@ -0,0 +1,323 @@ +// 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. + +import 'dart:io' as io; +import 'package:path/path.dart' as pathlib; +import 'package:web_driver_installer/chrome_driver_installer.dart'; + +import 'chrome_installer.dart'; +import 'environment.dart'; +import 'exceptions.dart'; +import 'utils.dart'; + +class IntegrationTestsManager { + final String _browser; + + /// Installation directory for browser's driver. + /// + /// Always re-install since driver can change frequently. + /// It usually changes with each the browser version changes. + /// A better solution would be installing the browser and the driver at the + /// same time. + // TODO(nurhan): https://github.com/flutter/flutter/issues/53179. + final io.Directory _browserDriverDir; + + /// This is the parent directory for all drivers. + /// + /// This directory is saved to [temporaryDirectories] and deleted before + /// tests shutdown. + final io.Directory _drivers; + + final bool _useSystemFlutter; + + IntegrationTestsManager(this._browser, this._useSystemFlutter) + : this._browserDriverDir = io.Directory(pathlib.join( + environment.webUiDartToolDir.path, 'drivers', _browser)), + this._drivers = io.Directory( + pathlib.join(environment.webUiDartToolDir.path, 'drivers')); + + Future runTests() async { + if (_browser != 'chrome') { + print('WARNING: integration tests are only supported on chrome for now'); + return false; + } else { + await prepareDriver(); + // TODO(nurhan): https://github.com/flutter/flutter/issues/52987 + return await _runTests(); + } + } + + Future _runPubGet(String workingDirectory) async { + if (!_useSystemFlutter) { + await _cloneFlutterRepo(); + await _enableWeb(workingDirectory); + } + await runFlutter(workingDirectory, ['pub', 'get'], + useSystemFlutter: _useSystemFlutter); + } + + /// Clone flutter repository, use the youngest commit older than the engine + /// commit. + /// + /// Use engine/src/flutter/.dart_tools to clone the Flutter repo. + /// TODO(nurhan): Use git pull instead if repo exists. + Future _cloneFlutterRepo() async { + // Delete directory if exists. + if (environment.engineDartToolDir.existsSync()) { + environment.engineDartToolDir.deleteSync(); + } + environment.engineDartToolDir.createSync(); + + final int exitCode = await runProcess( + environment.cloneFlutterScript.path, + [ + environment.engineDartToolDir.path, + ], + workingDirectory: environment.webUiRootDir.path, + ); + + if (exitCode != 0) { + throw ToolException('ERROR: Failed to clone flutter repo. Exited with ' + 'exit code $exitCode'); + } + } + + Future _enableWeb(String workingDirectory) async { + await runFlutter(workingDirectory, ['config', '--enable-web'], + useSystemFlutter: _useSystemFlutter); + } + + void _runDriver() async { + startProcess('./chromedriver/chromedriver', ['--port=4444'], + workingDirectory: io.Directory.current.path); + print('INFO: Driver started'); + } + + void prepareDriver() async { + if (_browserDriverDir.existsSync()) { + _browserDriverDir.deleteSync(recursive: true); + } + + _browserDriverDir.createSync(recursive: true); + temporaryDirectories.add(_drivers); + + io.Directory temp = io.Directory.current; + io.Directory.current = _browserDriverDir; + + // TODO(nurhan): https://github.com/flutter/flutter/issues/53179 + final String chromeDriverVersion = await queryChromeDriverVersion(); + ChromeDriverInstaller chromeDriverInstaller = + ChromeDriverInstaller.withVersion(chromeDriverVersion); + await chromeDriverInstaller.install(alwaysInstall: true); + await _runDriver(); + io.Directory.current = temp; + } + + /// Runs all the web tests under e2e_tests/web. + Future _runTests() async { + // Only list the files under e2e_tests/web. + final List entities = + environment.integrationTestsDir.listSync(followLinks: false); + + bool allTestsPassed = true; + for (io.FileSystemEntity e in entities) { + // The tests should be under this directories. + if (e is io.Directory) { + allTestsPassed = allTestsPassed && await _validateAndRunTests(e); + } + } + return allTestsPassed; + } + + /// Run tests in a single directory under: e2e_tests/web. + /// + /// Run `flutter pub get` as the first step. + /// + /// Validate the directory before running the tests. Each directory is + /// expected to be a test project which includes a `pubspec.yaml` file + /// and a `test_driver` directory. + Future _validateAndRunTests(io.Directory directory) async { + _validateTestDirectory(directory); + await _runPubGet(directory.path); + final bool testResults = await _runTestsInDirectory(directory); + return testResults; + } + + Future _runTestsInDirectory(io.Directory directory) async { + final io.Directory testDirectory = + io.Directory(pathlib.join(directory.path, 'test_driver')); + final List entities = testDirectory + .listSync(followLinks: false) + .whereType() + .toList(); + + final List e2eTestsToRun = List(); + + // The following loops over the contents of the directory and saves an + // expected driver file name for each e2e test assuming any dart file + // not ending with `_test.dart` is an e2e test. + // Other files are not considered since developers can add files such as + // README. + for (io.File f in entities) { + final String basename = pathlib.basename(f.path); + if (!basename.contains('_test.dart') && basename.endsWith('.dart')) { + e2eTestsToRun.add(basename); + } + } + + print( + 'INFO: In project ${directory} ${e2eTestsToRun.length} tests to run.'); + + int numberOfPassedTests = 0; + int numberOfFailedTests = 0; + for (String fileName in e2eTestsToRun) { + final bool testResults = + await _runTestsInProfileMode(directory, fileName); + if (testResults) { + numberOfPassedTests++; + } else { + numberOfFailedTests++; + } + } + final int numberOfTestsRun = numberOfPassedTests + numberOfFailedTests; + + print('INFO: ${numberOfTestsRun} tests run. ${numberOfPassedTests} passed ' + 'and ${numberOfFailedTests} failed.'); + return numberOfFailedTests == 0; + } + + Future _runTestsInProfileMode( + io.Directory directory, String testName) async { + final String executable = + _useSystemFlutter ? 'flutter' : environment.flutterCommand.path; + final int exitCode = await runProcess( + executable, + [ + 'drive', + '--target=test_driver/${testName}', + '-d', + 'web-server', + '--profile', + '--browser-name=$_browser', + '--local-engine=host_debug_unopt', + ], + workingDirectory: directory.path, + ); + + if (exitCode != 0) { + final String statementToRun = 'flutter drive ' + '--target=test_driver/${testName} -d web-server --profile ' + '--browser-name=$_browser --local-engine=host_debug_unopt'; + io.stderr + .writeln('ERROR: Failed to run test. Exited with exit code $exitCode' + '. Statement to run $testName locally use the following ' + 'command:\n\n$statementToRun'); + return false; + } else { + return true; + } + } + + /// Validate the directory has a `pubspec.yaml` file and a `test_driver` + /// directory. + /// + /// Also check the validity of files under `test_driver` directory calling + /// [_checkE2ETestsValidity] method. + void _validateTestDirectory(io.Directory directory) { + final List entities = + directory.listSync(followLinks: false); + + // Whether the project has the pubspec.yaml file. + bool pubSpecFound = false; + // The test directory 'test_driver'. + io.Directory testDirectory = null; + + for (io.FileSystemEntity e in entities) { + // The tests should be under this directories. + final String baseName = pathlib.basename(e.path); + if (e is io.Directory && baseName == 'test_driver') { + testDirectory = e; + } + if (e is io.File && baseName == 'pubspec.yaml') { + pubSpecFound = true; + } + } + if (!pubSpecFound) { + throw StateError('ERROR: pubspec.yaml file not found in the test project ' + 'in the directory ${directory.path}.'); + } + if (testDirectory == null) { + throw StateError( + 'ERROR: test_driver folder not found in the test project.' + 'in the directory ${directory.path}.'); + } else { + _checkE2ETestsValidity(testDirectory); + } + } + + /// Checks if each e2e test file in the directory has a driver test + /// file to run it. + /// + /// Prints informative message to the developer if an error has found. + /// For each e2e test which has name {name}.dart there will be a driver + /// file which drives it. The driver file should be named: + /// {name}_test.dart + void _checkE2ETestsValidity(io.Directory testDirectory) { + final Iterable directories = + testDirectory.listSync(followLinks: false).whereType(); + + if (directories.length > 0) { + throw StateError('${testDirectory.path} directory should not contain ' + 'any sub-directories'); + } + + final Iterable entities = + testDirectory.listSync(followLinks: false).whereType(); + + final Set expectedDriverFileNames = Set(); + final Set foundDriverFileNames = Set(); + int numberOfTests = 0; + + // The following loops over the contents of the directory and saves an + // expected driver file name for each e2e test assuming any file + // not ending with `_test.dart` is an e2e test. + for (io.File f in entities) { + final String basename = pathlib.basename(f.path); + if (basename.contains('_test.dart')) { + // First remove this from expectedSet if not there add to the foundSet. + if (!expectedDriverFileNames.remove(basename)) { + foundDriverFileNames.add(basename); + } + } else if (basename.contains('.dart')) { + // Only run on dart files. + final String e2efileName = pathlib.basenameWithoutExtension(f.path); + final String expectedDriverName = '${e2efileName}_test.dart'; + numberOfTests++; + // First remove this from foundSet if not there add to the expectedSet. + if (!foundDriverFileNames.remove(expectedDriverName)) { + expectedDriverFileNames.add(expectedDriverName); + } + } + } + + if (numberOfTests == 0) { + throw StateError( + 'WARNING: No tests to run in this directory ${testDirectory.path}'); + } + + // TODO(nurhan): In order to reduce the work required from team members, + // remove the need for driver file, by using the same template file. + // Some driver files are missing. + if (expectedDriverFileNames.length > 0) { + for (String expectedDriverName in expectedDriverFileNames) { + print('ERROR: Test driver file named has ${expectedDriverName} ' + 'not found under directory ${testDirectory.path}. Stopping the ' + 'integration tests. Please add ${expectedDriverName}. Check to ' + 'README file on more details on how to setup integration tests.'); + } + throw StateError('Error in test files. Check the logs for ' + 'further instructions'); + } + } +} diff --git a/lib/web_ui/dev/safari_installation.dart b/lib/web_ui/dev/safari_installation.dart index 05b0065d05fed..95c7dc2332edb 100644 --- a/lib/web_ui/dev/safari_installation.dart +++ b/lib/web_ui/dev/safari_installation.dart @@ -35,7 +35,7 @@ class SafariArgParser extends BrowserArgParser { @override void parseOptions(ArgResults argResults) { - _version = argResults['safari-version']; + _version = argResults['safari-version'] as String; assert(_version == 'system'); } diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 6b4663d5d4a91..922b432cabe04 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -152,21 +152,29 @@ class BrowserPlatform extends PlatformPlugin { } final String payload = await request.readAsString(); - final Map requestData = json.decode(payload); - final String filename = requestData['filename']; - final bool write = requestData['write']; + final Map requestData = + json.decode(payload) as Map; + final String filename = requestData['filename'] as String; + final bool write = requestData['write'] as bool; final double maxDiffRate = requestData.containsKey('maxdiffrate') - ? requestData['maxdiffrate'].toDouble() // can be parsed as either int or double - : kMaxDiffRateFailure; - final Map region = requestData['region']; - final PixelComparison pixelComparison = PixelComparison.values.firstWhere((value) => value.toString() == requestData['pixelComparison']); - final String result = await _diffScreenshot(filename, write, maxDiffRate, region, pixelComparison); + ? (requestData['maxdiffrate'] as num) + .toDouble() // can be parsed as either int or double + : kMaxDiffRateFailure; + final Map region = + requestData['region'] as Map; + final PixelComparison pixelComparison = PixelComparison.values.firstWhere( + (value) => value.toString() == requestData['pixelComparison']); + final String result = await _diffScreenshot( + filename, write, maxDiffRate, region, pixelComparison); return shelf.Response.ok(json.encode(result)); } Future _diffScreenshot( - String filename, bool write, double maxDiffRateFailure, - Map region, PixelComparison pixelComparison) async { + String filename, + bool write, + double maxDiffRateFailure, + Map region, + PixelComparison pixelComparison) async { if (doUpdateScreenshotGoldens) { write = true; } @@ -209,9 +217,9 @@ To automatically create this file call matchGoldenFile('$filename', write: true) Map captureScreenshotParameters = null; if (region != null) { - captureScreenshotParameters = { + captureScreenshotParameters = { 'format': 'png', - 'clip': { + 'clip': { 'x': region['x'], 'y': region['y'], 'width': region['width'], @@ -224,7 +232,8 @@ To automatically create this file call matchGoldenFile('$filename', write: true) // Setting hardware-independent screen parameters: // https://chromedevtools.github.io/devtools-protocol/tot/Emulation - await wipConnection.sendCommand('Emulation.setDeviceMetricsOverride', { + await wipConnection + .sendCommand('Emulation.setDeviceMetricsOverride', { 'width': kMaxScreenshotWidth, 'height': kMaxScreenshotHeight, 'deviceScaleFactor': 1, @@ -234,7 +243,8 @@ To automatically create this file call matchGoldenFile('$filename', write: true) 'Page.captureScreenshot', captureScreenshotParameters); // Compare screenshots - final Image screenshot = decodePng(base64.decode(response.result['data'])); + final Image screenshot = + decodePng(base64.decode(response.result['data'] as String)); if (write) { // Don't even bother with the comparison, just write and return @@ -406,17 +416,25 @@ Golden file $filename did not match the image generated by the test. '.'); } - if (_closed) return null; + if (_closed) { + return null; + } Uri suiteUrl = url.resolveUri( p.toUri(p.withoutExtension(p.relative(path, from: _root)) + '.html')); - if (_closed) return null; + if (_closed) { + return null; + } var browserManager = await _browserManagerFor(browser); - if (_closed || browserManager == null) return null; + if (_closed || browserManager == null) { + return null; + } var suite = await browserManager.load(path, suiteUrl, suiteConfig, message); - if (_closed) return null; + if (_closed) { + return null; + } return suite; } @@ -429,14 +447,16 @@ Golden file $filename did not match the image generated by the test. /// /// If no browser manager is running yet, starts one. Future _browserManagerFor(Runtime browser) { - if (_browserManager != null) return _browserManager; + if (_browserManager != null) { + return _browserManager; + } var completer = Completer.sync(); var path = _webSocketHandler.create(webSocketHandler(completer.complete)); var webSocketUrl = url.replace(scheme: 'ws').resolve(path); var hostUrl = (_config.pubServeUrl == null ? url : _config.pubServeUrl) .resolve('packages/web_engine_tester/static/index.html') - .replace(queryParameters: { + .replace(queryParameters: { 'managerUrl': webSocketUrl.toString(), 'debug': _config.pauseAfterLoad.toString() }); @@ -446,7 +466,7 @@ Golden file $filename did not match the image generated by the test. // Store null values for browsers that error out so we know not to load them // again. - _browserManager = future.catchError((_) => null); + _browserManager = future.catchError((dynamic _) => null); return future; } @@ -485,7 +505,7 @@ Golden file $filename did not match the image generated by the test. }); } - final _closeMemo = AsyncMemoizer(); + final AsyncMemoizer _closeMemo = AsyncMemoizer(); } /// A Shelf handler that provides support for one-time handlers. @@ -518,11 +538,15 @@ class OneOffHandler { /// Dispatches [request] to the appropriate handler. FutureOr _onRequest(shelf.Request request) { var components = p.url.split(request.url.path); - if (components.isEmpty) return shelf.Response.notFound(null); + if (components.isEmpty) { + return shelf.Response.notFound(null); + } var path = components.removeAt(0); var handler = _handlers.remove(path); - if (handler == null) return shelf.Response.notFound(null); + if (handler == null) { + return shelf.Response.notFound(null); + } return handler(request.change(path: path)); } } @@ -563,13 +587,19 @@ class PathHandler { var components = p.url.split(request.url.path); for (var i = 0; i < components.length; i++) { node = node.children[components[i]]; - if (node == null) break; - if (node.handler == null) continue; + if (node == null) { + break; + } + if (node.handler == null) { + continue; + } handler = node.handler; handlerIndex = i; } - if (handler == null) return shelf.Response.notFound('Not found.'); + if (handler == null) { + return shelf.Response.notFound('Not found.'); + } return handler( request.change(path: p.url.joinAll(components.take(handlerIndex + 1)))); @@ -623,7 +653,7 @@ class BrowserManager { CancelableCompleter _pauseCompleter; /// The controller for [_BrowserEnvironment.onRestart]. - final _onRestartController = StreamController.broadcast(); + final _onRestartController = StreamController.broadcast(); /// The environment to attach to each suite. Future<_BrowserEnvironment> _environment; @@ -660,17 +690,23 @@ class BrowserManager { browser.onExit.then((_) { throw Exception('${runtime.name} exited before connecting.'); - }).catchError((error, StackTrace stackTrace) { - if (completer.isCompleted) return; + }).catchError((dynamic error, StackTrace stackTrace) { + if (completer.isCompleted) { + return; + } completer.completeError(error, stackTrace); }); future.then((webSocket) { - if (completer.isCompleted) return; + if (completer.isCompleted) { + return; + } completer.complete(BrowserManager._(browser, runtime, webSocket)); - }).catchError((error, StackTrace stackTrace) { + }).catchError((dynamic error, StackTrace stackTrace) { browser.close(); - if (completer.isCompleted) return; + if (completer.isCompleted) { + return; + } completer.completeError(error, stackTrace); }); @@ -683,8 +719,7 @@ class BrowserManager { /// Starts the browser identified by [browser] using [settings] and has it load [url]. /// /// If [debug] is true, starts the browser in debug mode. - static Browser _newBrowser(Uri url, Runtime browser, - {bool debug = false}) { + static Browser _newBrowser(Uri url, Runtime browser, {bool debug = false}) { return SupportedBrowsers.instance.getBrowser(browser, url, debug: debug); } @@ -706,10 +741,12 @@ class BrowserManager { // Whenever we get a message, no matter which child channel it's for, we the // know browser is still running code which means the user isn't debugging. - _channel = MultiChannel( + _channel = MultiChannel( webSocket.cast().transform(jsonDocument).changeStream((stream) { return stream.map((message) { - if (!_closed) _timer.reset(); + if (!_closed) { + _timer.reset(); + } for (var controller in _controllers) { controller.setDebugging(false); } @@ -720,7 +757,7 @@ class BrowserManager { _environment = _loadBrowserEnvironment(); _channel.stream - .listen((message) => _onMessage(message as Map), onDone: close); + .listen((dynamic message) => _onMessage(message as Map), onDone: close); } /// Loads [_BrowserEnvironment]. @@ -737,15 +774,17 @@ class BrowserManager { Future load(String path, Uri url, SuiteConfiguration suiteConfig, Object message) async { url = url.replace( - fragment: Uri.encodeFull(jsonEncode({ + fragment: Uri.encodeFull(jsonEncode({ 'metadata': suiteConfig.metadata.serialize(), 'browser': _runtime.identifier }))); var suiteID = _suiteID++; RunnerSuiteController controller; - closeIframe() { - if (_closed) return; + void closeIframe() { + if (_closed) { + return; + } _controllers.remove(controller); _channel.sink.add({'command': 'closeSuite', 'id': suiteID}); } @@ -754,8 +793,8 @@ class BrowserManager { // case we should unload the iframe. var virtualChannel = _channel.virtualChannel(); var suiteChannelID = virtualChannel.id; - var suiteChannel = virtualChannel - .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) { + var suiteChannel = virtualChannel.transformStream( + StreamTransformer.fromHandlers(handleDone: (sink) { closeIframe(); sink.close(); })); @@ -797,9 +836,11 @@ class BrowserManager { /// An implementation of [Environment.displayPause]. CancelableOperation _displayPause() { - if (_pauseCompleter != null) return _pauseCompleter.operation; + if (_pauseCompleter != null) { + return _pauseCompleter.operation; + } - _pauseCompleter = CancelableCompleter(onCancel: () { + _pauseCompleter = CancelableCompleter(onCancel: () { _channel.sink.add({'command': 'resume'}); _pauseCompleter = null; }); @@ -824,7 +865,9 @@ class BrowserManager { break; case 'resume': - if (_pauseCompleter != null) _pauseCompleter.complete(); + if (_pauseCompleter != null) { + _pauseCompleter.complete(); + } break; default: @@ -839,12 +882,14 @@ class BrowserManager { Future close() => _closeMemoizer.runOnce(() { _closed = true; _timer.cancel(); - if (_pauseCompleter != null) _pauseCompleter.complete(); + if (_pauseCompleter != null) { + _pauseCompleter.complete(); + } _pauseCompleter = null; _controllers.clear(); return _browser.close(); }); - final _closeMemoizer = AsyncMemoizer(); + final AsyncMemoizer _closeMemoizer = AsyncMemoizer(); } /// An implementation of [Environment] for the browser. diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 0779ad450fad3..598f85d9fb717 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -15,12 +15,26 @@ import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_im import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports +import 'environment.dart'; +import 'exceptions.dart'; +import 'integration_tests_manager.dart'; import 'supported_browsers.dart'; import 'test_platform.dart'; -import 'environment.dart'; import 'utils.dart'; -class TestCommand extends Command { +/// The type of tests requested by the tool user. +enum TestTypesRequested { + /// For running the unit tests only. + unit, + + /// For running the integration tests only. + integration, + + /// For running both unit and integration tests. + all, +} + +class TestCommand extends Command with ArgUtils { TestCommand() { argParser ..addFlag( @@ -29,6 +43,31 @@ class TestCommand extends Command { 'opportunity to add breakpoints or inspect loaded code before ' 'running the code.', ) + ..addFlag( + 'unit-tests-only', + defaultsTo: false, + help: 'felt test command runs the unit tests and the integration tests ' + 'at the same time. If this flag is set, only run the unit tests.', + ) + ..addFlag( + 'integration-tests-only', + defaultsTo: false, + help: 'felt test command runs the unit tests and the integration tests ' + 'at the same time. If this flag is set, only run the integration ' + 'tests.', + ) + ..addFlag('use-system-flutter', + defaultsTo: false, + help: 'integration tests are using flutter repository for various tasks' + ', such as flutter drive, flutter pub get. If this flag is set, felt ' + 'will use flutter command without cloning the repository. This flag ' + 'can save internet bandwidth. However use with caution. Note that ' + 'since flutter repo is always synced to youngest commit older than ' + 'the engine commit for the tests running in CI, the tests results ' + 'won\'t be consistent with CIs when this flag is set. flutter ' + 'command should be set in the PATH for this flag to be useful.' + 'This flag can also be used to test local Flutter changes.' + ) ..addFlag( 'update-screenshot-goldens', defaultsTo: false, @@ -54,11 +93,68 @@ class TestCommand extends Command { @override final String description = 'Run tests.'; + TestTypesRequested testTypesRequested = null; + + /// Check the flags to see what type of tests are requested. + TestTypesRequested findTestType() { + if (boolArg('unit-tests-only') && boolArg('integration-tests-only')) { + throw ArgumentError('Conflicting arguments: unit-tests-only and ' + 'integration-tests-only are both set'); + } else if (boolArg('unit-tests-only')) { + print('Running the unit tests only'); + return TestTypesRequested.unit; + } else if (boolArg('integration-tests-only')) { + if (!isChrome) { + throw UnimplementedError( + 'Integration tests are only available on Chrome Desktop for now'); + } + return TestTypesRequested.integration; + } else { + return TestTypesRequested.all; + } + } + @override Future run() async { SupportedBrowsers.instance ..argParsers.forEach((t) => t.parseOptions(argResults)); + // Check the flags to see what type of integration tests are requested. + testTypesRequested = findTestType(); + + switch (testTypesRequested) { + case TestTypesRequested.unit: + return runUnitTests(); + case TestTypesRequested.integration: + return runIntegrationTests(); + case TestTypesRequested.all: + // TODO(nurhan): https://github.com/flutter/flutter/issues/53322 + // TODO(nurhan): Expand browser matrix for felt integration tests. + if (runAllTests && isChrome) { + bool integrationTestResult = await runIntegrationTests(); + bool unitTestResult = await runUnitTests(); + if (integrationTestResult != unitTestResult) { + print('Tests run. Integration tests passed: $integrationTestResult ' + 'unit tests passed: $unitTestResult'); + } + return integrationTestResult && unitTestResult; + } else { + return await runUnitTests(); + } + } + return false; + } + + Future runIntegrationTests() async { + // TODO(nurhan): https://github.com/flutter/flutter/issues/52983 + if (io.Platform.environment['LUCI_CONTEXT'] != null) { + return true; + } + + return IntegrationTestsManager(browser, useSystemFlutter).runTests(); + } + + Future runUnitTests() async { _copyTestFontsIntoWebUi(); await _buildHostPage(); if (io.Platform.isWindows) { @@ -68,13 +164,31 @@ class TestCommand extends Command { await _runPubGet(); } - final List targets = - this.targets.map((t) => FilePath.fromCwd(t)).toList(); - await _buildTests(targets: targets); - if (targets.isEmpty) { + await _buildTests(targets: targetFiles); + + // Many tabs will be left open after Safari runs, quit Safari during + // cleanup. + if (browser == 'safari') { + cleanupCallbacks.add(() async { + // Only close Safari if felt is running in CI environments. Do not close + // Safari for the local testing. + if (io.Platform.environment['LUCI_CONTEXT'] != null || isCirrus) { + print('INFO: Safari tests ran. Quit Safari.'); + await runProcess( + 'sudo', + ['pkill', '-lf', 'Safari'], + workingDirectory: environment.webUiRootDir.path, + ); + } else { + print('INFO: Safari tests ran. Please quit Safari tabs.'); + } + }); + } + + if (runAllTests) { await _runAllTests(); } else { - await _runTargetTests(targets); + await _runTargetTests(targetFiles); } return true; } @@ -83,18 +197,35 @@ class TestCommand extends Command { /// /// In this mode the browser pauses before running the test to allow /// you set breakpoints or inspect the code. - bool get isDebug => argResults['debug']; + bool get isDebug => boolArg('debug'); /// Paths to targets to run, e.g. a single test. List get targets => argResults.rest; - String get browser => argResults['browser']; + /// The target test files to run. + /// + /// The value can be null if the developer prefers to run all the tests. + List get targetFiles => (targets.isEmpty) + ? null + : targets.map((t) => FilePath.fromCwd(t)).toList(); + + /// Whether all tests should run. + bool get runAllTests => targets.isEmpty; + + /// The name of the browser to run tests in. + String get browser => (argResults != null) ? stringArg('browser') : 'chrome'; - bool get isChrome => argResults['browser'] == 'chrome'; + /// Whether [browser] is set to "chrome". + bool get isChrome => browser == 'chrome'; + + /// Use system flutter instead of cloning the repository. + /// + /// Read the flag help for more details. Uses PATH to locate flutter. + bool get useSystemFlutter => boolArg('use-system-flutter'); /// When running screenshot tests writes them to the file system into /// ".dart_tool/goldens". - bool get doUpdateScreenshotGoldens => argResults['update-screenshot-goldens']; + bool get doUpdateScreenshotGoldens => boolArg('update-screenshot-goldens'); Future _runTargetTests(List targets) async { await _runTestBatch(targets, concurrency: 1, expectFailure: false); @@ -183,8 +314,7 @@ class TestCommand extends Command { void _checkExitCode() { if (io.exitCode != 0) { - io.stderr.writeln('Process exited with exit code ${io.exitCode}.'); - io.exit(1); + throw ToolException('Process exited with exit code ${io.exitCode}.'); } } @@ -198,9 +328,8 @@ class TestCommand extends Command { ); if (exitCode != 0) { - io.stderr - .writeln('Failed to run pub get. Exited with exit code $exitCode'); - io.exit(1); + throw ToolException( + 'Failed to run pub get. Exited with exit code $exitCode'); } } @@ -241,9 +370,8 @@ class TestCommand extends Command { ); if (exitCode != 0) { - io.stderr.writeln( - 'Failed to compile ${hostDartFile.path}. Compiler exited with exit code $exitCode'); - io.exit(1); + throw ToolException('Failed to compile ${hostDartFile.path}. Compiler ' + 'exited with exit code $exitCode'); } // Record the timestamp to avoid rebuilding unless the file changes. @@ -252,18 +380,18 @@ class TestCommand extends Command { Future _buildTests({List targets}) async { List arguments = [ - 'run', - 'build_runner', - 'build', - 'test', - '-o', - 'build', - if (targets != null) - for (FilePath path in targets) ...[ - '--build-filter=${path.relativeToWebUi}.js', - '--build-filter=${path.relativeToWebUi}.browser_test.dart.js', - ], - ]; + 'run', + 'build_runner', + 'build', + 'test', + '-o', + 'build', + if (targets != null) + for (FilePath path in targets) ...[ + '--build-filter=${path.relativeToWebUi}.js', + '--build-filter=${path.relativeToWebUi}.browser_test.dart.js', + ], + ]; final int exitCode = await runProcess( environment.pubExecutable, arguments, @@ -271,9 +399,8 @@ class TestCommand extends Command { ); if (exitCode != 0) { - io.stderr.writeln( + throw ToolException( 'Failed to compile tests. Compiler exited with exit code $exitCode'); - io.exit(1); } } diff --git a/lib/web_ui/dev/utils.dart b/lib/web_ui/dev/utils.dart index d60df326b3eea..2ccdb04c5f049 100644 --- a/lib/web_ui/dev/utils.dart +++ b/lib/web_ui/dev/utils.dart @@ -6,10 +6,12 @@ import 'dart:async'; import 'dart:io' as io; +import 'package:args/command_runner.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'environment.dart'; +import 'exceptions.dart'; class FilePath { FilePath.fromCwd(String relativePath) @@ -62,6 +64,25 @@ Future runProcess( return exitCode; } +/// Runs [executable]. Do not follow the exit code or the output. +void startProcess( + String executable, + List arguments, { + String workingDirectory, + bool mustSucceed: false, +}) async { + final io.Process process = await io.Process.start( + executable, + arguments, + workingDirectory: workingDirectory, + // Running the process in a system shell for Windows. Otherwise + // the process is not able to get Dart from path. + runInShell: io.Platform.isWindows, + mode: io.ProcessStartMode.inheritStdio, + ); + processesToCleanUp.add(process); +} + /// Runs [executable] and returns its standard output as a string. /// /// If the process fails, throws a [ProcessException]. @@ -77,14 +98,34 @@ Future evalProcess( ); if (result.exitCode != 0) { throw ProcessException( - description: result.stderr, + description: result.stderr as String, executable: executable, arguments: arguments, workingDirectory: workingDirectory, exitCode: result.exitCode, ); } - return result.stdout; + return result.stdout as String; +} + +Future runFlutter( + String workingDirectory, + List arguments, { + bool useSystemFlutter = false, +}) async { + final String executable = + useSystemFlutter ? 'flutter' : environment.flutterCommand.path; + arguments.add('--local-engine=host_debug_unopt'); + final int exitCode = await runProcess( + executable, + arguments, + workingDirectory: workingDirectory, + ); + + if (exitCode != 0) { + throw ToolException('ERROR: Failed to run $executable with ' + 'arguments ${arguments.toString()}. Exited with exit code $exitCode'); + } } @immutable @@ -109,8 +150,74 @@ class ProcessException implements Exception { message ..writeln(description) ..writeln('Command: $executable ${arguments.join(' ')}') - ..writeln('Working directory: ${workingDirectory ?? io.Directory.current.path}') + ..writeln( + 'Working directory: ${workingDirectory ?? io.Directory.current.path}') ..writeln('Exit code: $exitCode'); return '$message'; } } + +/// Adds utility methods +mixin ArgUtils on Command { + /// Extracts a boolean argument from [argResults]. + bool boolArg(String name) => argResults[name] as bool; + + /// Extracts a string argument from [argResults]. + String stringArg(String name) => argResults[name] as String; + + /// Extracts a integer argument from [argResults]. + /// + /// If the argument value cannot be parsed as [int] throws an [ArgumentError]. + int intArg(String name) { + final String rawValue = stringArg(name); + if (rawValue == null) { + return null; + } + final int value = int.tryParse(rawValue); + if (value == null) { + throw ArgumentError( + 'Argument $name should be an integer value but was "$rawValue"', + ); + } + return value; + } +} + +/// There might be proccesses started during the tests. +/// +/// Use this list to store those Processes, for cleaning up before shutdown. +final List processesToCleanUp = List(); + +/// There might be temporary directories created during the tests. +/// +/// Use this list to store those directories and for deleteing them before +/// shutdown. +final List temporaryDirectories = List(); + +typedef AsyncCallback = Future Function(); + +/// There might be additional cleanup needs to be done after the tools ran. +/// +/// Add these operations here to make sure that they will run before felt +/// exit. +final List cleanupCallbacks = List(); + +/// Cleanup the remaning processes, close open browsers, delete temp files. +void cleanup() async { + // Cleanup remaining processes if any. + if (processesToCleanUp.length > 0) { + for (io.Process process in processesToCleanUp) { + process.kill(); + } + } + // Delete temporary directories. + if (temporaryDirectories.length > 0) { + for (io.Directory directory in temporaryDirectories) { + directory.deleteSync(recursive: true); + } + } + + cleanupCallbacks.forEach((element) { + element.call(); + }); +} diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 00b90d9bb4b9d..9af6c4f7887f2 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -69,6 +69,7 @@ part 'engine/platform_views.dart'; part 'engine/plugins.dart'; part 'engine/pointer_binding.dart'; part 'engine/pointer_converter.dart'; +part 'engine/profiler.dart'; part 'engine/render_vertices.dart'; part 'engine/rrect_renderer.dart'; part 'engine/semantics/accessibility.dart'; @@ -117,6 +118,7 @@ part 'engine/text_editing/text_editing.dart'; part 'engine/util.dart'; part 'engine/validators.dart'; part 'engine/vector_math.dart'; +part 'engine/web_experiments.dart'; part 'engine/window.dart'; bool _engineInitialized = false; @@ -161,6 +163,12 @@ void webOnlyInitializeEngine() { // initialize framework bindings. domRenderer; + WebExperiments.ensureInitialized(); + + if (Profiler.isBenchmarkMode) { + Profiler.ensureInitialized(); + } + bool waitingForAnimation = false; ui.webOnlyScheduleFrameCallback = () { // We're asked to schedule a frame and call `frameHandler` when the frame @@ -178,17 +186,17 @@ void webOnlyInitializeEngine() { // microsecond precision, and only then convert to `int`. final int highResTimeMicroseconds = (1000 * highResTime).toInt(); - if (ui.window.onBeginFrame != null) { - ui.window - .onBeginFrame(Duration(microseconds: highResTimeMicroseconds)); + if (window._onBeginFrame != null) { + window + .invokeOnBeginFrame(Duration(microseconds: highResTimeMicroseconds)); } - if (ui.window.onDrawFrame != null) { + if (window._onDrawFrame != null) { // TODO(yjbanov): technically Flutter flushes microtasks between // onBeginFrame and onDrawFrame. We don't, which hasn't // been an issue yet, but eventually we'll have to // implement it properly. - ui.window.onDrawFrame(); + window.invokeOnDrawFrame(); } }); } diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index c9fdb211b9677..e0ba89020de02 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -355,15 +355,18 @@ class BitmapCanvas extends EngineCanvas { void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { _drawImage(image, p, paint); _childOverdraw = true; - _canvasPool.allocateExtraCanvas(); + _canvasPool.closeCurrentCanvas(); } - html.ImageElement _drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { + html.ImageElement _drawImage( + ui.Image image, ui.Offset p, SurfacePaintData paint) { final HtmlImage htmlImage = image; final html.Element imgElement = htmlImage.cloneImageElement(); final ui.BlendMode blendMode = paint.blendMode; imgElement.style.mixBlendMode = _stringForBlendMode(blendMode); if (_canvasPool.isClipped) { + // Reset width/height since they may have been previously set. + imgElement.style..removeProperty('width')..removeProperty('height'); final List clipElements = _clipContent( _canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform); for (html.Element clipElement in clipElements) { @@ -375,7 +378,10 @@ class BitmapCanvas extends EngineCanvas { transformWithOffset(_canvasPool.currentTransform, p).storage); imgElement.style ..transformOrigin = '0 0 0' - ..transform = cssTransform; + ..transform = cssTransform + // Reset width/height since they may have been previously set. + ..removeProperty('width') + ..removeProperty('height'); rootElement.append(imgElement); _children.add(imgElement); } @@ -411,7 +417,8 @@ class BitmapCanvas extends EngineCanvas { } } - final html.ImageElement imgElement = _drawImage(image, ui.Offset(targetLeft, targetTop), paint); + final html.ImageElement imgElement = + _drawImage(image, ui.Offset(targetLeft, targetTop), paint); // To scale set width / height on destination image. // For clipping we need to scale according to // clipped-width/full image width and shift it according to left/top of @@ -429,8 +436,22 @@ class BitmapCanvas extends EngineCanvas { if (requiresClipping) { restore(); } - _canvasPool.allocateExtraCanvas(); } + _closeCurrentCanvas(); + } + + // Should be called when we add new html elements into rootElement so that + // paint order is preserved. + // + // For example if we draw a path and then a paragraph and image: + // - rootElement + // |--- + // |---

+ // |--- + // Any drawing operations after these tags should allocate a new canvas, + // instead of drawing into earlier canvas. + void _closeCurrentCanvas() { + _canvasPool.closeCurrentCanvas(); _childOverdraw = true; } @@ -498,7 +519,6 @@ class BitmapCanvas extends EngineCanvas { final html.Element paragraphElement = _drawParagraphElement(paragraph, offset); - if (_canvasPool.isClipped) { final List clipElements = _clipContent( _canvasPool._clipStack, @@ -517,6 +537,7 @@ class BitmapCanvas extends EngineCanvas { rootElement.append(paragraphElement); } _children.add(paragraphElement); + _closeCurrentCanvas(); } /// Paints the [picture] into this canvas. @@ -768,6 +789,8 @@ List _clipContent(List<_SaveClipEntry> clipStack, } String _maskFilterToCss(ui.MaskFilter maskFilter) { - if (maskFilter == null) return 'none'; + if (maskFilter == null) { + return 'none'; + } return 'blur(${maskFilter.webOnlySigma}px)'; } diff --git a/lib/web_ui/lib/src/engine/browser_location.dart b/lib/web_ui/lib/src/engine/browser_location.dart index e2f0cfc4d58ab..30bcfcdf64c37 100644 --- a/lib/web_ui/lib/src/engine/browser_location.dart +++ b/lib/web_ui/lib/src/engine/browser_location.dart @@ -9,27 +9,6 @@ part of engine; // Some parts of this file were inspired/copied from the AngularDart router. -/// Ensures that `str` is prefixed with `leading`. -/// -/// If `str` is already prefixed, it'll be returned unchanged. If it's not, -/// this function will prefix it. -/// -/// The `applyWhenEmpty` flag controls whether this function should prefix `str` -/// or not when it's an empty string. -/// -/// ```dart -/// ensureLeading('/path', '/'); // "/path" -/// ensureLeading('path', '/'); // "/path" -/// ensureLeading('', '/'); // "/" -/// ensureLeading('', '/', applyWhenEmpty: false); // "" -/// ``` -String ensureLeading(String str, String leading, {bool applyWhenEmpty = true}) { - if (str.isEmpty && !applyWhenEmpty) { - return str; - } - return str.startsWith(leading) ? str : '$leading$str'; -} - /// [LocationStrategy] is responsible for representing and reading route state /// from the browser's URL. /// @@ -93,12 +72,11 @@ class HashLocationStrategy extends LocationStrategy { // the hash value is always prefixed with a `#` // and if it is empty then it will stay empty String path = _platformLocation.hash ?? ''; + assert(path.isEmpty || path.startsWith('#')); // Dart will complain if a call to substring is // executed with a position value that exceeds the // length of string. - path = path.isEmpty ? path : path.substring(1); - // The path, by convention, should always contain a leading '/'. - return ensureLeading(path, '/'); + return path.isEmpty ? path : path.substring(1); } @override diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index b5119afb82c10..56d8020023359 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -9,7 +9,7 @@ part of engine; /// /// [BitmapCanvas] signals allocation of first canvas using allocateCanvas. /// When a painting command such as drawImage or drawParagraph requires -/// multiple canvases for correct compositing, it calls [allocateExtraCanvas] +/// multiple canvases for correct compositing, it calls [closeCurrentCanvas] /// and adds the canvas(s) to a [_pool] of active canvas(s). /// /// To make sure transformations and clips are preserved correctly when a new @@ -54,9 +54,12 @@ class _CanvasPool extends _SaveStackTracking { return _contextHandle; } - // Allocating extra canvas items. Save current canvas so we can dispose + // Prevents active canvas to be used for rendering and prepares a new + // canvas allocation on next drawing request that will require one. + // + // Saves current canvas so we can dispose // and replay the clip/transform stack on top of new canvas. - void allocateExtraCanvas() { + void closeCurrentCanvas() { assert(_rootElement != null); // Place clean copy of current canvas with context stack restored and paint // reset into pool. @@ -79,6 +82,10 @@ class _CanvasPool extends _SaveStackTracking { bool requiresClearRect = false; if (_reusablePool != null && _reusablePool.isNotEmpty) { _canvas = _reusablePool.removeAt(0); + // If a canvas is the first element we set z-index = -1 to workaround + // blink compositing bug. To make sure this does not leak when reused + // reset z-index. + _canvas.style.removeProperty('z-index'); requiresClearRect = true; } else { // Compute the final CSS canvas size given the actual pixel count we @@ -613,7 +620,12 @@ class _CanvasPool extends _SaveStackTracking { context.save(); context.filter = 'none'; context.strokeStyle = ''; - context.fillStyle = colorToCssString(color); + final int red = color.red; + final int green = color.green; + final int blue = color.blue; + // Multiply by 0.4 to make shadows less aggressive (https://github.com/flutter/flutter/issues/52734) + final int alpha = (0.4 * color.alpha).round(); + context.fillStyle = colorComponentsToCssString(red, green, blue, alpha); context.shadowBlur = shadow.blurWidth; context.shadowColor = colorToCssString(color.withAlpha(0xff)); context.shadowOffsetX = shadow.offset.dx; diff --git a/lib/web_ui/lib/src/engine/clipboard.dart b/lib/web_ui/lib/src/engine/clipboard.dart index fb82e4a566aeb..cc5ed5532c96c 100644 --- a/lib/web_ui/lib/src/engine/clipboard.dart +++ b/lib/web_ui/lib/src/engine/clipboard.dart @@ -18,6 +18,7 @@ class ClipboardMessageHandler { void setDataMethodCall( MethodCall methodCall, ui.PlatformMessageResponseCallback callback) { const MethodCodec codec = JSONMethodCodec(); + bool errorEnvelopeEncoded = false; _copyToClipboardStrategy .setData(methodCall.arguments['text']) .then((bool success) { @@ -26,10 +27,15 @@ class ClipboardMessageHandler { } else { callback(codec.encodeErrorEnvelope( code: 'copy_fail', message: 'Clipboard.setData failed')); + errorEnvelopeEncoded = true; + } + }).catchError((dynamic _) { + // Don't encode a duplicate reply if we already failed and an error + // was already encoded. + if (!errorEnvelopeEncoded) { + callback(codec.encodeErrorEnvelope( + code: 'copy_fail', message: 'Clipboard.setData failed')); } - }).catchError((_) { - callback(codec.encodeErrorEnvelope( - code: 'copy_fail', message: 'Clipboard.setData failed')); }); } @@ -37,9 +43,9 @@ class ClipboardMessageHandler { void getDataMethodCall(ui.PlatformMessageResponseCallback callback) { const MethodCodec codec = JSONMethodCodec(); _pasteFromClipboardStrategy.getData().then((String data) { - final Map map = {'text': data}; + final Map map = {'text': data}; callback(codec.encodeSuccessEnvelope(map)); - }).catchError((error) { + }).catchError((dynamic error) { print('Could not get text from clipboard: $error'); callback(codec.encodeErrorEnvelope( code: 'paste_fail', message: 'Clipboard.getData failed')); diff --git a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart b/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart index f58ed7e76dfb4..0aca26c381f11 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart @@ -145,7 +145,7 @@ class CanvasKitCanvas implements ui.Canvas { _drawLine(p1, p2, paint); } - void _drawLine(ui.Offset p1, ui.Offset p2, paint) { + void _drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) { _canvas.drawLine(p1, p2, paint); } diff --git a/lib/web_ui/lib/src/engine/compositor/color_filter.dart b/lib/web_ui/lib/src/engine/compositor/color_filter.dart index 8a5857066942d..38aecfee0cadc 100644 --- a/lib/web_ui/lib/src/engine/compositor/color_filter.dart +++ b/lib/web_ui/lib/src/engine/compositor/color_filter.dart @@ -19,7 +19,7 @@ class SkColorFilter { SkColorFilter.matrix(EngineColorFilter filter) { // TODO(het): Find a way to remove these array conversions. - final js.JsArray colorMatrix = js.JsArray(); + final js.JsArray colorMatrix = js.JsArray(); colorMatrix.length = 20; for (int i = 0; i < 20; i++) { colorMatrix[i] = filter._matrix[i]; diff --git a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart b/lib/web_ui/lib/src/engine/compositor/embedded_views.dart index 31f9dc176bafe..60d218d0d0e47 100644 --- a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/compositor/embedded_views.dart @@ -314,7 +314,9 @@ class HtmlViewEmbedder { /// Ensures we add a container of SVG path defs to the DOM so they can /// be referred to in clip-path: url(#blah). void _ensureSvgPathDefs() { - if (_svgPathDefs != null) return; + if (_svgPathDefs != null) { + return; + } _svgPathDefs = html.Element.html( '', treeSanitizer: _NullTreeSanitizer(), @@ -397,8 +399,12 @@ class EmbeddedViewParams { final MutatorsStack mutators; bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! EmbeddedViewParams) return false; + if (identical(this, other)) { + return true; + } + if (other is! EmbeddedViewParams) { + return false; + } EmbeddedViewParams typedOther = other; return offset == typedOther.offset && @@ -454,8 +460,12 @@ class Mutator { double get alphaFloat => alpha / 255.0; bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! Mutator) return false; + if (identical(this, other)) { + return true; + } + if (other is! Mutator) { + return false; + } final Mutator typedOther = other; if (type != typedOther.type) { @@ -515,8 +525,12 @@ class MutatorsStack extends Iterable { } bool operator ==(dynamic other) { - if (identical(other, this)) return true; - if (other is! MutatorsStack) return false; + if (identical(other, this)) { + return true; + } + if (other is! MutatorsStack) { + return false; + } final MutatorsStack typedOther = other; if (_mutators.length != typedOther._mutators.length) { diff --git a/lib/web_ui/lib/src/engine/compositor/fonts.dart b/lib/web_ui/lib/src/engine/compositor/fonts.dart index f6d23be754c96..99c62fa9e91e1 100644 --- a/lib/web_ui/lib/src/engine/compositor/fonts.dart +++ b/lib/web_ui/lib/src/engine/compositor/fonts.dart @@ -47,7 +47,9 @@ class SkiaFontCollection { /// Loads all of the unloaded fonts in [_unloadedFonts] and adds them /// to [_registeredFonts]. Future _loadFonts() async { - if (_unloadedFonts.isEmpty) return; + if (_unloadedFonts.isEmpty) { + return; + } final List<_RegisteredFont> loadedFonts = await Future.wait(_unloadedFonts); _registeredFonts.addAll(loadedFonts.where((x) => x != null)); @@ -174,7 +176,8 @@ class SkiaFontCollection { } Future _getArrayBuffer(dynamic fetchResult) { - return fetchResult.arrayBuffer().then((x) => x as ByteBuffer); + // TODO(yjbanov): fetchResult.arrayBuffer is a dynamic invocation. Clean it up. + return fetchResult.arrayBuffer().then((dynamic x) => x as ByteBuffer); } js.JsObject skFontMgr; diff --git a/lib/web_ui/lib/src/engine/compositor/surface.dart b/lib/web_ui/lib/src/engine/compositor/surface.dart index 3e5afed3fe64e..3f8a2c7880479 100644 --- a/lib/web_ui/lib/src/engine/compositor/surface.dart +++ b/lib/web_ui/lib/src/engine/compositor/surface.dart @@ -50,7 +50,9 @@ class Surface { final SkSurface surface = acquireRenderSurface(size); canvasKit.callMethod('setCurrentContext', [surface.context]); - if (surface == null) return null; + if (surface == null) { + return null; + } SubmitCallback submitCallback = (SurfaceFrame surfaceFrame, SkCanvas canvas) { @@ -112,10 +114,14 @@ class Surface { ..position = 'absolute' ..width = '${logicalSize.width.ceil()}px' ..height = '${logicalSize.height.ceil()}px'; - final int glContext = canvasKit - .callMethod('GetWebGLContext', [htmlCanvas]); + final int glContext = canvasKit.callMethod('GetWebGLContext', [ + htmlCanvas, + // Default to no anti-aliasing. Paint commands can be explicitly + // anti-aliased by setting their `Paint` object's `antialias` property. + js.JsObject.jsify({'antialias': 0}), + ]); final js.JsObject grContext = - canvasKit.callMethod('MakeGrContext', [glContext]); + canvasKit.callMethod('MakeGrContext', [glContext]); final js.JsObject skSurface = canvasKit.callMethod('MakeOnScreenGLSurface', [ grContext, @@ -137,7 +143,7 @@ class Surface { return false; } - canvasKit.callMethod('setCurrentContext', [_surface.context]); + canvasKit.callMethod('setCurrentContext', [_surface.context]); _surface.getCanvas().flush(); return true; } diff --git a/lib/web_ui/lib/src/engine/compositor/vertices.dart b/lib/web_ui/lib/src/engine/compositor/vertices.dart index 67164c498411c..6cde7a8ec8374 100644 --- a/lib/web_ui/lib/src/engine/compositor/vertices.dart +++ b/lib/web_ui/lib/src/engine/compositor/vertices.dart @@ -8,7 +8,9 @@ part of engine; Int32List _encodeColorList(List colors) { final int colorCount = colors.length; final Int32List result = Int32List(colorCount); - for (int i = 0; i < colorCount; ++i) result[i] = colors[i].value; + for (int i = 0; i < colorCount; ++i) { + result[i] = colors[i].value; + } return result; } @@ -116,8 +118,10 @@ class SkVertices implements ui.Vertices { } } - static _encodePoints(List points) { - if (points == null) return null; + static js.JsArray> _encodePoints(List points) { + if (points == null) { + return null; + } js.JsArray> encodedPoints = js.JsArray>(); diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 8e2f585b83f20..b6f1cd06c4366 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -71,7 +71,7 @@ class DomRenderer { /// This getter calls the `hasFocus` method of the `Document` interface. /// See for more details: /// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus - bool get windowHasFocus => js_util.callMethod(html.document, 'hasFocus', []); + bool get windowHasFocus => js_util.callMethod(html.document, 'hasFocus', []); void _setupHotRestart() { // This persists across hot restarts to clear stale DOM. @@ -419,12 +419,6 @@ flt-glass-pane * { // DOM tree. setElementAttribute(_sceneHostElement, 'aria-hidden', 'true'); - // We treat browser pixels as device pixels because pointer events, - // position, and sizes all use browser pixel as the unit (i.e. "px" in CSS). - // Therefore, as far as the framework is concerned the device pixel ratio - // is 1.0. - window.debugOverrideDevicePixelRatio(1.0); - if (html.window.visualViewport == null && isWebKit) { // Safari sometimes gives us bogus innerWidth/innerHeight values when the // page loads. When it changes the values to correct ones it does not @@ -473,8 +467,8 @@ flt-glass-pane * { /// Called immediately after browser window metrics change. void _metricsDidChange(html.Event event) { window._computePhysicalSize(); - if (ui.window.onMetricsChanged != null) { - ui.window.onMetricsChanged(); + if (window._onMetricsChanged != null) { + window.invokeOnMetricsChanged(); } } diff --git a/lib/web_ui/lib/src/engine/history.dart b/lib/web_ui/lib/src/engine/history.dart index 0c46004dc0117..ad7558f7ecfc5 100644 --- a/lib/web_ui/lib/src/engine/history.dart +++ b/lib/web_ui/lib/src/engine/history.dart @@ -94,8 +94,8 @@ class BrowserHistory { _setupFlutterEntry(_locationStrategy); // 2. Send a 'popRoute' platform message so the app can handle it accordingly. - if (ui.window.onPlatformMessage != null) { - ui.window.onPlatformMessage( + if (window._onPlatformMessage != null) { + window.invokeOnPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall(_popRouteMethodCall), (_) {}, @@ -113,8 +113,8 @@ class BrowserHistory { _userProvidedRouteName = null; // Send a 'pushRoute' platform message so the app handles it accordingly. - if (ui.window.onPlatformMessage != null) { - ui.window.onPlatformMessage( + if (window._onPlatformMessage != null) { + window.invokeOnPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall( MethodCall('pushRoute', newRouteName), diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index ee48e62c541dd..c380258d90411 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -4,16 +4,21 @@ // @dart = 2.6 part of engine; + final bool _supportsDecode = js_util.getProperty( js_util.getProperty( js_util.getProperty(html.window, 'Image'), 'prototype'), 'decode') != null; +typedef WebOnlyImageCodecChunkCallback = void Function( + int cumulativeBytesLoaded, int expectedTotalBytes); + class HtmlCodec implements ui.Codec { final String src; + final WebOnlyImageCodecChunkCallback chunkCallback; - HtmlCodec(this.src); + HtmlCodec(this.src, {this.chunkCallback}); @override int get frameCount => 1; @@ -24,18 +29,27 @@ class HtmlCodec implements ui.Codec { @override Future getNextFrame() async { final Completer completer = Completer(); + // Currently there is no way to watch decode progress, so + // we add 0/100 , 100/100 progress callbacks to enable loading progress + // builders to create UI. + if (chunkCallback != null) { + chunkCallback(0, 100); + } if (_supportsDecode) { final html.ImageElement imgElement = html.ImageElement(); imgElement.src = src; js_util.setProperty(imgElement, 'decoding', 'async'); imgElement.decode().then((dynamic _) { + if (chunkCallback != null) { + chunkCallback(100, 100); + } final HtmlImage image = HtmlImage( imgElement, imgElement.naturalWidth, imgElement.naturalHeight, ); completer.complete(SingleFrameInfo(image)); - }).catchError((e) { + }).catchError((dynamic e) { // This code path is hit on Chrome 80.0.3987.16 when too many // images are on the page (~1000). // Fallback here is to load using onLoad instead. @@ -61,6 +75,9 @@ class HtmlCodec implements ui.Codec { completer.completeError(event); }); loadSubscription = imgElement.onLoad.listen((html.Event event) { + if (chunkCallback != null) { + chunkCallback(100, 100); + } loadSubscription.cancel(); errorSubscription.cancel(); final HtmlImage image = HtmlImage( diff --git a/lib/web_ui/lib/src/engine/keyboard.dart b/lib/web_ui/lib/src/engine/keyboard.dart index d42b44fc6f829..f9408d1438b42 100644 --- a/lib/web_ui/lib/src/engine/keyboard.dart +++ b/lib/web_ui/lib/src/engine/keyboard.dart @@ -5,22 +5,6 @@ // @dart = 2.6 part of engine; -/// Contains a whitelist of keys that must be sent to Flutter under all -/// circumstances. -/// -/// When keys are pressed in a text field, we generally don't want to send them -/// to Flutter. This list of keys is the exception to that rule. Keys in this -/// list will be sent to Flutter even if pressed in a text field. -/// -/// A good example is the "Tab" and "Shift" keys which are used by the framework -/// to move focus between text fields. -const List _alwaysSentKeys = [ - 'Alt', - 'Control', - 'Meta', - 'Shift', - 'Tab', -]; /// Provides keyboard bindings, such as the `flutter/keyevent` channel. class Keyboard { @@ -68,11 +52,7 @@ class Keyboard { static const JSONMessageCodec _messageCodec = JSONMessageCodec(); void _handleHtmlEvent(html.KeyboardEvent event) { - if (ui.window.onPlatformMessage == null) { - return; - } - - if (_shouldIgnoreEvent(event)) { + if (window._onPlatformMessage == null) { return; } @@ -88,24 +68,10 @@ class Keyboard { 'metaState': _getMetaState(event), }; - ui.window.onPlatformMessage('flutter/keyevent', + window.invokeOnPlatformMessage('flutter/keyevent', _messageCodec.encodeMessage(eventData), _noopCallback); } - /// Whether the [Keyboard] class should ignore the given [html.KeyboardEvent]. - /// - /// When this method returns true, it prevents the keyboard event from being - /// sent to Flutter. - bool _shouldIgnoreEvent(html.KeyboardEvent event) { - // Keys in the [_alwaysSentKeys] list should never be ignored. - if (_alwaysSentKeys.contains(event.key)) { - return false; - } - // Other keys should be ignored if triggered on a text field. - return event.target is html.Element && - HybridTextEditing.isEditingElement(event.target); - } - bool _shouldPreventDefault(html.KeyboardEvent event) { switch (event.key) { case 'Tab': diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 6b6726f3b6ce7..48e43dc72db45 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -125,9 +125,8 @@ class PointerBinding { void _onPointerData(Iterable data) { final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data.toList()); - final ui.PointerDataPacketCallback callback = ui.window.onPointerDataPacket; - if (callback != null) { - callback(packet); + if (window._onPointerDataPacket != null) { + window.invokeOnPointerDataPacket(packet); } } } @@ -167,7 +166,7 @@ abstract class _BaseAdapter { /// Remove all active event listeners. void clearListeners() { _listeners.forEach((String eventName, html.EventListener listener) { - glassPaneElement.removeEventListener(eventName, listener, true); + html.window.removeEventListener(eventName, listener, true); }); // For native listener, we will need to remove it through native javascript // api. @@ -184,8 +183,23 @@ abstract class _BaseAdapter { _nativeListeners.clear(); } - void addEventListener(String eventName, html.EventListener handler) { + /// Adds a listener to the given [eventName]. + /// + /// The event listener is attached to [html.window] but only events that have + /// [glassPaneElement] as a target will be let through by default. + /// + /// If [acceptOutsideGlasspane] is set to true, events outside of the + /// glasspane will also invoke the [handler]. + void addEventListener( + String eventName, + html.EventListener handler, { + bool acceptOutsideGlasspane = false, + }) { final html.EventListener loggedHandler = (html.Event event) { + if (!acceptOutsideGlasspane && !glassPaneElement.contains(event.target)) { + return; + } + if (_debugLogPointerEvents) { print(event.type); } @@ -197,8 +211,11 @@ abstract class _BaseAdapter { } }; _listeners[eventName] = loggedHandler; - glassPaneElement - .addEventListener(eventName, loggedHandler, true); + // We have to attach the event listener on the window instead of the + // glasspane element. That's because "up" events that occur outside the + // browser are only reported on window, not on DOM elements. + // See: https://github.com/flutter/flutter/issues/52827 + html.window.addEventListener(eventName, loggedHandler, true); } /// Converts a floating number timestamp (in milliseconds) to a [Duration] by @@ -413,11 +430,15 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { } } - void _addPointerEventListener(String eventName, _PointerEventListener handler) { + void _addPointerEventListener( + String eventName, + _PointerEventListener handler, { + bool acceptOutsideGlasspane = false, + }) { addEventListener(eventName, (html.Event event) { final html.PointerEvent pointerEvent = event; return handler(pointerEvent); - }); + }, acceptOutsideGlasspane: acceptOutsideGlasspane); } @override @@ -445,7 +466,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _convertEventsToPointerData(data: pointerData, event: event, details: details); } _callback(pointerData); - }); + }, acceptOutsideGlasspane: true); _addPointerEventListener('pointerup', (html.PointerEvent event) { final int device = event.pointerId; @@ -456,7 +477,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _convertEventsToPointerData(data: pointerData, event: event, details: details); } _callback(pointerData); - }); + }, acceptOutsideGlasspane: true); // A browser fires cancel event if it concludes the pointer will no longer // be able to generate events (example: device is deactivated) @@ -707,11 +728,15 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { final _ButtonSanitizer _sanitizer = _ButtonSanitizer(); - void _addMouseEventListener(String eventName, _MouseEventListener handler) { + void _addMouseEventListener( + String eventName, + _MouseEventListener handler, { + bool acceptOutsideGlasspane = false, + }) { addEventListener(eventName, (html.Event event) { final html.MouseEvent mouseEvent = event; return handler(mouseEvent); - }); + }, acceptOutsideGlasspane: acceptOutsideGlasspane); } @override @@ -732,7 +757,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { final _SanitizedDetails sanitizedDetails = _sanitizer.sanitizeMoveEvent(buttons: event.buttons); _convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails); _callback(pointerData); - }); + }, acceptOutsideGlasspane: true); _addMouseEventListener('mouseup', (html.MouseEvent event) { final List pointerData = []; @@ -742,7 +767,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { _sanitizer.sanitizeMoveEvent(buttons: event.buttons); _convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails); _callback(pointerData); - }); + }, acceptOutsideGlasspane: true); _addWheelEventListener((html.Event event) { assert(event is html.WheelEvent); diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart new file mode 100644 index 0000000000000..383f480917042 --- /dev/null +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -0,0 +1,72 @@ +// 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. + +// @dart = 2.6 +part of engine; + +typedef OnBenchmark = void Function(String name, num value); + +/// The purpose of this class is to facilitate communication of +/// profiling/benchmark data to the outside world (e.g. a macrobenchmark that's +/// running a flutter app). +/// +/// To use the [Profiler]: +/// +/// 1. Set the environment variable `FLUTTER_WEB_ENABLE_PROFILING` to true. +/// +/// 2. Using JS interop, assign a listener function to +/// `window._flutter_internal_on_benchmark` in the browser. +/// +/// The listener function will be called every time a new benchmark number is +/// calculated. The signature is `Function(String name, num value)`. +class Profiler { + Profiler._() { + _checkBenchmarkMode(); + } + + static bool isBenchmarkMode = const bool.fromEnvironment( + 'FLUTTER_WEB_ENABLE_PROFILING', + defaultValue: true, + ); + + static Profiler ensureInitialized() { + _checkBenchmarkMode(); + return Profiler._instance ??= Profiler._(); + } + + static Profiler get instance { + _checkBenchmarkMode(); + if (_instance == null) { + throw Exception( + 'Profiler has not been properly initialized. ' + 'Make sure Profiler.ensureInitialized() is being called before you ' + 'access Profiler.instance', + ); + } + return _instance; + } + + static Profiler _instance; + + static void _checkBenchmarkMode() { + if (!isBenchmarkMode) { + throw Exception( + 'Cannot use Profiler unless benchmark mode is enabled. ' + 'You can enable it by setting the `FLUTTER_WEB_ENABLE_PROFILING` ' + 'environment variable to true.', + ); + } + } + + /// Used to send benchmark data to whoever is listening to them. + void benchmark(String name, num value) { + _checkBenchmarkMode(); + + final OnBenchmark onBenchmark = + js_util.getProperty(html.window, '_flutter_internal_on_benchmark'); + if (onBenchmark != null) { + onBenchmark(name, value); + } + } +} diff --git a/lib/web_ui/lib/src/engine/render_vertices.dart b/lib/web_ui/lib/src/engine/render_vertices.dart index d39ccd469d169..89849fe463065 100644 --- a/lib/web_ui/lib/src/engine/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/render_vertices.dart @@ -145,7 +145,7 @@ class _WebGlRenderer implements _GlRenderer { gl.bindArrayBuffer(positionsBuffer); gl.bufferData(positions, gl.kStaticDraw); js_util.callMethod( - gl.glContext, 'vertexAttribPointer', [0, 2, gl.kFloat, false, 0, 0]); + gl.glContext, 'vertexAttribPointer', [0, 2, gl.kFloat, false, 0, 0]); gl.enableVertexAttribArray(0); // Setup color buffer. @@ -155,7 +155,7 @@ class _WebGlRenderer implements _GlRenderer { gl.bufferData(vertices.colors, gl.kStaticDraw); js_util.callMethod(gl.glContext, 'vertexAttribPointer', - [1, 4, gl.kUnsignedByte, true, 0, 0]); + [1, 4, gl.kUnsignedByte, true, 0, 0]); gl.enableVertexAttribArray(1); gl.clear(); final int vertexCount = positions.length ~/ 2; @@ -317,7 +317,7 @@ class _GlContext { static Map _programCache; _GlContext.fromOffscreenCanvas(html.OffscreenCanvas canvas) - : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false}), + : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false}), isOffscreen = true { _programCache = {}; _canvas = canvas; @@ -325,7 +325,7 @@ class _GlContext { _GlContext.fromCanvas(html.CanvasElement canvas, bool useWebGl1) : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', - {'premultipliedAlpha': false}), + {'premultipliedAlpha': false}), isOffscreen = false { _programCache = {}; _canvas = canvas; @@ -342,7 +342,7 @@ class _GlContext { // Actual size of canvas may be larger than viewport size. Use // source/destination to draw part of the image data. js_util.callMethod(context, 'drawImage', - [_canvas, 0, 0, _widthInPixels, _heightInPixels, + [_canvas, 0, 0, _widthInPixels, _heightInPixels, left, top, _widthInPixels, _heightInPixels]); } @@ -372,10 +372,10 @@ class _GlContext { if (shader == null) { throw Exception(error); } - js_util.callMethod(glContext, 'shaderSource', [shader, source]); - js_util.callMethod(glContext, 'compileShader', [shader]); + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); bool shaderStatus = js_util - .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); + .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); if (!shaderStatus) { throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); } @@ -383,73 +383,73 @@ class _GlContext { } Object createProgram() => - js_util.callMethod(glContext, 'createProgram', const []); + js_util.callMethod(glContext, 'createProgram', const []); void attachShader(Object program, Object shader) { - js_util.callMethod(glContext, 'attachShader', [program, shader]); + js_util.callMethod(glContext, 'attachShader', [program, shader]); } void linkProgram(Object program) { - js_util.callMethod(glContext, 'linkProgram', [program]); + js_util.callMethod(glContext, 'linkProgram', [program]); if (!js_util - .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { + .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { throw Exception(getProgramInfoLog(program)); } } void useProgram(Object program) { - js_util.callMethod(glContext, 'useProgram', [program]); + js_util.callMethod(glContext, 'useProgram', [program]); } Object createBuffer() => - js_util.callMethod(glContext, 'createBuffer', const []); + js_util.callMethod(glContext, 'createBuffer', const []); void bindArrayBuffer(Object buffer) { - js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); + js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); } void deleteBuffer(Object buffer) { - js_util.callMethod(glContext, 'deleteBuffer', [buffer]); + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); } void bufferData(TypedData data, dynamic type) { - js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); } void enableVertexAttribArray(int index) { - js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); } /// Clear background. void clear() { - js_util.callMethod(glContext, 'clear', [kColorBufferBit]); + js_util.callMethod(glContext, 'clear', [kColorBufferBit]); } /// Destroys gl context. void dispose() { - js_util.callMethod(_getExtension('WEBGL_lose_context'), 'loseContext', []); + js_util.callMethod(_getExtension('WEBGL_lose_context'), 'loseContext', const []); } void deleteProgram(Object program) { - js_util.callMethod(glContext, 'deleteProgram', [program]); + js_util.callMethod(glContext, 'deleteProgram', [program]); } void deleteShader(Object shader) { - js_util.callMethod(glContext, 'deleteShader', [shader]); + js_util.callMethod(glContext, 'deleteShader', [shader]); } dynamic _getExtension(String extensionName) => - js_util.callMethod(glContext, 'getExtension', [extensionName]); + js_util.callMethod(glContext, 'getExtension', [extensionName]); void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { dynamic mode = _triangleTypeFromMode(vertexMode); - js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); + js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); } /// Sets affine transformation from normalized device coordinates /// to window coordinates void viewport(double x, double y, double width, double height) { - js_util.callMethod(glContext, 'viewport', [x, y, width, height]); + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); } dynamic _triangleTypeFromMode(ui.VertexMode mode) { @@ -467,10 +467,10 @@ class _GlContext { } Object _createShader(String shaderType) => js_util.callMethod( - glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); /// Error state of gl context. - dynamic get error => js_util.callMethod(glContext, 'getError', const []); + dynamic get error => js_util.callMethod(glContext, 'getError', const []); /// Shader compiler error, if this returns [kFalse], to get details use /// [getShaderInfoLog]. @@ -508,37 +508,37 @@ class _GlContext { /// Returns reference to uniform in program. Object getUniformLocation(Object program, String uniformName) { return js_util - .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); } /// Sets vec2 uniform values. void setUniform2f(Object uniform, double value1, double value2) { return js_util - .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); + .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); } /// Sets vec4 uniform values. void setUniform4f(Object uniform, double value1, double value2, double value3, double value4) { return js_util.callMethod( - glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); + glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); } /// Sets mat4 uniform values. void setUniformMatrix4fv(Object uniform, bool transpose, Float64List value) { return js_util.callMethod( - glContext, 'uniformMatrix4fv', [uniform, transpose, value]); + glContext, 'uniformMatrix4fv', [uniform, transpose, value]); } /// Shader compile error log. dynamic getShaderInfoLog(Object glShader) { - return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); + return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); } /// Errors that occurred during failed linking or validation of program /// objects. Typically called after [linkProgram]. String getProgramInfoLog(Object glProgram) { - return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); } int get drawingBufferWidth => @@ -555,7 +555,7 @@ class _GlContext { final Uint8List pixels = Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData( Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); } else { @@ -565,7 +565,7 @@ class _GlContext { final Uint8ClampedList pixels = Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData(pixels, bufferWidth, bufferHeight); } } diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index a858ebc7ab668..2d874f8cf01da 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -52,9 +52,9 @@ class AccessibilityAnnouncements { html.HtmlElement get _domElement => _element ??= _createElement(); /// Decodes the message coming from the 'flutter/accessibility' channel. - void handleMessage(ByteData data) { + void handleMessage(StandardMessageCodec codec, ByteData data) { final Map inputMap = - const StandardMessageCodec().decodeMessage(data); + codec.decodeMessage(data); final Map dataMap = inputMap['data']; final String message = dataMap['message']; if (message != null && message.isNotEmpty) { diff --git a/lib/web_ui/lib/src/engine/semantics/incrementable.dart b/lib/web_ui/lib/src/engine/semantics/incrementable.dart index e61baf76188d5..5ae975aa4fd1f 100644 --- a/lib/web_ui/lib/src/engine/semantics/incrementable.dart +++ b/lib/web_ui/lib/src/engine/semantics/incrementable.dart @@ -53,11 +53,11 @@ class Incrementable extends RoleManager { final int newInputValue = int.parse(_element.value); if (newInputValue > _currentSurrogateValue) { _currentSurrogateValue += 1; - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.increase, null); } else if (newInputValue < _currentSurrogateValue) { _currentSurrogateValue -= 1; - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.decrease, null); } }); diff --git a/lib/web_ui/lib/src/engine/semantics/scrollable.dart b/lib/web_ui/lib/src/engine/semantics/scrollable.dart index 0f847d631098e..0c818314a26f1 100644 --- a/lib/web_ui/lib/src/engine/semantics/scrollable.dart +++ b/lib/web_ui/lib/src/engine/semantics/scrollable.dart @@ -53,20 +53,20 @@ class Scrollable extends RoleManager { final int semanticsId = semanticsObject.id; if (doScrollForward) { if (semanticsObject.isVerticalScrollContainer) { - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsId, ui.SemanticsAction.scrollUp, null); } else { assert(semanticsObject.isHorizontalScrollContainer); - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsId, ui.SemanticsAction.scrollLeft, null); } } else { if (semanticsObject.isVerticalScrollContainer) { - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsId, ui.SemanticsAction.scrollDown, null); } else { assert(semanticsObject.isHorizontalScrollContainer); - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsId, ui.SemanticsAction.scrollRight, null); } } diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 0554a1a48350a..a37fb7e19bae9 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -1236,8 +1236,8 @@ class EngineSemanticsOwner { _gestureModeClock?.datetime = null; } - if (ui.window.onSemanticsEnabledChanged != null) { - ui.window.onSemanticsEnabledChanged(); + if (window._onSemanticsEnabledChanged != null) { + window.invokeOnSemanticsEnabledChanged(); } } diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart index 25b94819d4882..e60c9881918e5 100644 --- a/lib/web_ui/lib/src/engine/semantics/tappable.dart +++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart @@ -39,7 +39,7 @@ class Tappable extends RoleManager { GestureMode.browserGestures) { return; } - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.tap, null); }; element.addEventListener('click', _clickListener); diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 4091ece5854db..5018d8d2d5aa2 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -149,8 +149,8 @@ class TextField extends RoleManager { } textEditing.useCustomEditableElement(textEditingElement); - ui.window - .onSemanticsAction(semanticsObject.id, ui.SemanticsAction.tap, null); + window + .invokeOnSemanticsAction(semanticsObject.id, ui.SemanticsAction.tap, null); }); } @@ -187,7 +187,7 @@ class TextField extends RoleManager { if (offsetX * offsetX + offsetY * offsetY < kTouchSlop) { // Recognize it as a tap that requires a keyboard. - ui.window.onSemanticsAction( + window.invokeOnSemanticsAction( semanticsObject.id, ui.SemanticsAction.tap, null); } } else { diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/shader.dart index 128e3a01e063f..a64140bc83751 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/shader.dart @@ -238,7 +238,25 @@ class GradientConical extends EngineGradient { @override js.JsObject createSkiaShader() { - throw UnimplementedError(); + assert(experimentalUseSkia); + + final js.JsArray jsColors = js.JsArray(); + jsColors.length = colors.length; + for (int i = 0; i < colors.length; i++) { + jsColors[i] = colors[i].value; + } + + return canvasKit.callMethod('MakeTwoPointConicalGradient', [ + makeSkPoint(focal), + focalRadius, + makeSkPoint(center), + radius, + jsColors, + makeSkiaColorStops(colorStops), + tileMode.index, + matrix4 != null ? makeSkMatrix(matrix4) : null, + 0, + ]); } } diff --git a/lib/web_ui/lib/src/engine/shadow.dart b/lib/web_ui/lib/src/engine/shadow.dart index 3c46dac7681b5..cd6abe34f9ace 100644 --- a/lib/web_ui/lib/src/engine/shadow.dart +++ b/lib/web_ui/lib/src/engine/shadow.dart @@ -45,7 +45,7 @@ const double kLightOffsetY = -400.0; /// This is not a complete physical model. For example, this does not take into /// account the size of the shape (this function doesn't even take the shape as /// a parameter). It's just a good enough approximation. -ui.Offset computeShadowOffset(elevation) { +ui.Offset computeShadowOffset(double elevation) { if (elevation == 0.0) { return ui.Offset.zero; } @@ -133,7 +133,9 @@ void applyCssShadow( if (shadow == null) { element.style.boxShadow = 'none'; } else { + // Multiply by 0.4 to make shadows less aggressive (https://github.com/flutter/flutter/issues/52734) + final double alpha = 0.4 * color.alpha / 255; element.style.boxShadow = '${shadow.offset.dx}px ${shadow.offset.dy}px ' - '${shadow.blurWidth}px 0px rgb(${color.red}, ${color.green}, ${color.blue})'; + '${shadow.blurWidth}px 0px rgba(${color.red}, ${color.green}, ${color.blue}, $alpha)'; } } diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/surface/painting.dart index def459d055032..59388db32493b 100644 --- a/lib/web_ui/lib/src/engine/surface/painting.dart +++ b/lib/web_ui/lib/src/engine/surface/painting.dart @@ -934,7 +934,9 @@ class SurfacePath implements ui.Path { EngineWindow.browserDevicePixelRatio) { _rawRecorder = null; } - _rawRecorder ??= ui.RawRecordingCanvas(size); + final double dpr = window.devicePixelRatio; + _rawRecorder ??= ui.RawRecordingCanvas(ui.Size(size.width / dpr, + size.height / dpr)); // Account for the shift due to padding. _rawRecorder.translate(-BitmapCanvas.kPaddingPixels.toDouble(), -BitmapCanvas.kPaddingPixels.toDouble()); diff --git a/lib/web_ui/lib/src/engine/surface/path_metrics.dart b/lib/web_ui/lib/src/engine/surface/path_metrics.dart index 98a95622d3c48..7ad30fd725dc3 100644 --- a/lib/web_ui/lib/src/engine/surface/path_metrics.dart +++ b/lib/web_ui/lib/src/engine/surface/path_metrics.dart @@ -616,7 +616,6 @@ class SurfacePathMetricIterator implements Iterator { SurfacePathMetric _pathMetric; _SurfacePathMeasure _pathMeasure; - bool _firstTime = true; @override SurfacePathMetric get current => _pathMetric; diff --git a/lib/web_ui/lib/src/engine/surface/scene_builder.dart b/lib/web_ui/lib/src/engine/surface/scene_builder.dart index 4de1fe779e55c..c61d183c83d24 100644 --- a/lib/web_ui/lib/src/engine/surface/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/surface/scene_builder.dart @@ -10,7 +10,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { _surfaceStack.add(PersistedScene(_lastFrameScene)); } - final List _surfaceStack = []; + final List _surfaceStack = + []; /// The scene built by this scene builder. /// @@ -19,7 +20,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { assert(() { if (_surfaceStack.length != 1) { final String surfacePrintout = _surfaceStack - .map((PersistedContainerSurface surface) => surface.runtimeType) + .map( + (PersistedContainerSurface surface) => surface.runtimeType) .toList() .join(', '); throw Exception('Incorrect sequence of push/pop operations while ' @@ -42,7 +44,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { // the live tree. if (surface.oldLayer != null) { assert(surface.oldLayer.runtimeType == surface.runtimeType); - assert(debugAssertSurfaceState(surface.oldLayer, PersistedSurfaceState.active)); + assert(debugAssertSurfaceState( + surface.oldLayer, PersistedSurfaceState.active)); surface.oldLayer.state = PersistedSurfaceState.pendingUpdate; } _adoptSurface(surface); @@ -97,6 +100,16 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { if (matrix4.length != 16) { throw ArgumentError('"matrix4" must have 16 entries.'); } + if (_surfaceStack.length == 1) { + // Top level transform contains view configuration to scale + // scene to devicepixelratio. Use identity instead since CSS uses + // logical device pixels. + if (!ui.debugEmulateFlutterTesterEnvironment) { + assert(matrix4[0] == window.devicePixelRatio && + matrix4[5] == window.devicePixelRatio); + } + matrix4 = Matrix4.identity().storage; + } return _pushSurface(PersistedTransform(oldLayer, matrix4)); } @@ -276,7 +289,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { void addRetained(ui.EngineLayer retainedLayer) { final PersistedContainerSurface retainedSurface = retainedLayer; if (assertionsEnabled) { - assert(debugAssertSurfaceState(retainedSurface, PersistedSurfaceState.active, PersistedSurfaceState.released)); + assert(debugAssertSurfaceState(retainedSurface, + PersistedSurfaceState.active, PersistedSurfaceState.released)); } retainedSurface.tryRetain(); _adoptSurface(retainedSurface); @@ -301,8 +315,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { /// controls where the statistics are displayed. /// /// enabledOptions is a bit field with the following bits defined: - /// - 0x01: displayRasterizerStatistics - show GPU thread frame time - /// - 0x02: visualizeRasterizerStatistics - graph GPU thread frame times + /// - 0x01: displayRasterizerStatistics - show raster thread frame time + /// - 0x02: visualizeRasterizerStatistics - graph raster thread frame times /// - 0x04: displayEngineStatistics - show UI thread frame time /// - 0x08: visualizeEngineStatistics - graph UI thread frame times /// Set enabledOptions to 0x0F to enable all the currently defined features. @@ -310,7 +324,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { /// The "UI thread" is the thread that includes all the execution of /// the main Dart isolate (the isolate that can call /// [Window.render]). The UI thread frame time is the total time - /// spent executing the [Window.onBeginFrame] callback. The "GPU + /// spent executing the [Window.onBeginFrame] callback. The "raster /// thread" is the thread (running on the CPU) that subsequently /// processes the [Scene] provided by the Dart code to turn it into /// GPU commands and send it to the GPU. @@ -319,7 +333,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { /// for more details. @override void addPerformanceOverlay(int enabledOptions, ui.Rect bounds) { - _addPerformanceOverlay(enabledOptions, bounds.left, bounds.right, bounds.top, bounds.bottom); + _addPerformanceOverlay( + enabledOptions, bounds.left, bounds.right, bounds.top, bounds.bottom); } /// Whether we've already warned the user about the lack of the performance @@ -337,7 +352,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ) { if (!_webOnlyDidWarnAboutPerformanceOverlay) { _webOnlyDidWarnAboutPerformanceOverlay = true; - html.window.console.warn('The performance overlay isn\'t supported on the web'); + html.window.console + .warn('The performance overlay isn\'t supported on the web'); } } @@ -377,7 +393,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { _addTexture(offset.dx, offset.dy, width, height, textureId); } - void _addTexture(double dx, double dy, double width, double height, int textureId) { + void _addTexture( + double dx, double dy, double width, double height, int textureId) { // In test mode, allow this to be a no-op. if (!ui.debugEmulateFlutterTesterEnvironment) { throw UnimplementedError('Textures are not supported in Flutter Web'); diff --git a/lib/web_ui/lib/src/engine/test_embedding.dart b/lib/web_ui/lib/src/engine/test_embedding.dart index 713fddf712ca6..c1295a2ff0921 100644 --- a/lib/web_ui/lib/src/engine/test_embedding.dart +++ b/lib/web_ui/lib/src/engine/test_embedding.dart @@ -37,7 +37,7 @@ class TestLocationStrategy extends LocationStrategy { history = [initialEntry]; @override - String get path => ensureLeading(currentEntry.url, '/'); + String get path => currentEntry.url; int _currentEntryIndex; int get currentEntryIndex => _currentEntryIndex; diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index 3781ff6412ef5..30ae994538c1a 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -187,13 +187,6 @@ abstract class TextMeasurementService { static TextMeasurementService get canvasInstance => CanvasTextMeasurementService.instance; - /// Whether the new experimental implementation of canvas-based text - /// measurement is enabled or not. - /// - /// This is only used for testing at the moment. Once the implementation is - /// complete and production-ready, we'll get rid of this flag. - static bool enableExperimentalCanvasImplementation = const bool.fromEnvironment('FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', defaultValue: false); - /// Gets the appropriate [TextMeasurementService] instance for the given /// [paragraph]. static TextMeasurementService forParagraph(ui.Paragraph paragraph) { @@ -206,7 +199,7 @@ abstract class TextMeasurementService { // Skip using canvas measurements until the iframe becomes visible. // see: https://github.com/flutter/flutter/issues/36341 if (!window.physicalSize.isEmpty && - enableExperimentalCanvasImplementation && + WebExperiments.instance.useCanvasText && _canUseCanvasMeasurement(paragraph)) { return canvasInstance; } diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 83ec07b875f25..3dbc6fbecafe1 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -255,7 +255,16 @@ class EngineParagraph implements ui.Paragraph { return; } + Stopwatch stopwatch; + if (Profiler.isBenchmarkMode) { + stopwatch = Stopwatch()..start(); + } _measurementResult = _measurementService.measure(this, constraints); + if (Profiler.isBenchmarkMode) { + stopwatch.stop(); + Profiler.instance.benchmark('text_layout', stopwatch.elapsedMicroseconds); + } + _lastUsedConstraints = constraints; if (_geometricStyle.maxLines != null) { diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index b2e317d27f5ea..38655a528d861 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -924,8 +924,8 @@ class MeasurementResult { @required this.alphabeticBaseline, @required this.ideographicBaseline, @required this.lines, - @required textAlign, - @required textDirection, + @required ui.TextAlign textAlign, + @required ui.TextDirection textDirection, }) : assert(constraintWidth != null), assert(isSingleLine != null), assert(width != null), diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 369d711e59f38..040e11f3036b3 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -778,8 +778,11 @@ class TextEditingChannel { final HybridTextEditing implementation; /// Handles "flutter/textinput" platform messages received from the framework. - void handleTextInput(ByteData data) { - final MethodCall call = const JSONMethodCodec().decodeMethodCall(data); + void handleTextInput( + ByteData data, + ui.PlatformMessageResponseCallback callback) { + const JSONMethodCodec codec = JSONMethodCodec(); + final MethodCall call = codec.decodeMethodCall(data); switch (call.method) { case 'TextInput.setClient': implementation.setClient( @@ -815,12 +818,13 @@ class TextEditingChannel { default: throw StateError('Unsupported method call on the flutter/textinput channel: ${call.method}'); } + window._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); } /// Sends the 'TextInputClient.updateEditingState' message to the framework. void updateEditingState(int clientId, EditingState editingState) { - if (ui.window.onPlatformMessage != null) { - ui.window.onPlatformMessage( + if (window._onPlatformMessage != null) { + window.invokeOnPlatformMessage( 'flutter/textinput', const JSONMethodCodec().encodeMethodCall( MethodCall('TextInputClient.updateEditingState', [ @@ -835,8 +839,8 @@ class TextEditingChannel { /// Sends the 'TextInputClient.performAction' message to the framework. void performAction(int clientId, String inputAction) { - if (ui.window.onPlatformMessage != null) { - ui.window.onPlatformMessage( + if (window._onPlatformMessage != null) { + window.invokeOnPlatformMessage( 'flutter/textinput', const JSONMethodCodec().encodeMethodCall( MethodCall( @@ -851,8 +855,8 @@ class TextEditingChannel { /// Sends the 'TextInputClient.onConnectionClosed' message to the framework. void onConnectionClosed(int clientId) { - if (ui.window.onPlatformMessage != null) { - ui.window.onPlatformMessage( + if (window._onPlatformMessage != null) { + window.invokeOnPlatformMessage( 'flutter/textinput', const JSONMethodCodec().encodeMethodCall( MethodCall( diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index e0d69b68bfbaa..4ca7c5c540eb9 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -349,6 +349,16 @@ String _colorToCssStringRgbOnly(ui.Color color) { return '#${paddedValue.substring(paddedValue.length - 6)}'; } +/// Converts color components to a CSS compatible attribute value. +String colorComponentsToCssString(int r, int g, int b, int a) { + if (a == 255) { + return 'rgb($r,$g,$b)'; + } else { + final double alphaRatio = a / 255; + return 'rgba($r,$g,$b,${alphaRatio.toStringAsFixed(2)})'; + } +} + /// Determines if the (dynamic) exception passed in is a NS_ERROR_FAILURE /// (from Firefox). /// @@ -434,3 +444,14 @@ void applyWebkitClipFix(html.Element containerElement) { containerElement.style.zIndex = '0'; } } + +final ByteData _fontChangeMessage = JSONMessageCodec().encodeMessage({'type': 'fontsChange'}); + +FutureOr sendFontChangeMessage() async { + if (window._onPlatformMessage != null) + window.invokeOnPlatformMessage( + 'flutter/system', + _fontChangeMessage, + (_) {}, + ); +} diff --git a/lib/web_ui/lib/src/engine/web_experiments.dart b/lib/web_ui/lib/src/engine/web_experiments.dart new file mode 100644 index 0000000000000..2e2050938e8df --- /dev/null +++ b/lib/web_ui/lib/src/engine/web_experiments.dart @@ -0,0 +1,53 @@ +// 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. + +// @dart = 2.6 +part of engine; + +/// A bag of all experiment flags in the web engine. +/// +/// This class also handles platform messages that can be sent to enable/disable +/// certain experiments at runtime without the need to access engine internals. +class WebExperiments { + WebExperiments._() { + js.context['_flutter_internal_update_experiment'] = updateExperiment; + registerHotRestartListener(() { + js.context['_flutter_internal_update_experiment'] = null; + }); + } + + static WebExperiments ensureInitialized() { + if (WebExperiments.instance == null) { + WebExperiments.instance = WebExperiments._(); + } + return WebExperiments.instance; + } + + static WebExperiments instance; + + /// Experiment flag for using canvas-based text measurement. + bool get useCanvasText => _useCanvasText ?? false; + set useCanvasText(bool enabled) { + _useCanvasText = enabled; + } + + bool _useCanvasText = const bool.fromEnvironment( + 'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', + defaultValue: null, + ); + + /// Reset all experimental flags to their default values. + void reset() { + _useCanvasText = null; + } + + /// Used to enable/disable experimental flags in the web engine. + void updateExperiment(String name, bool enabled) { + switch (name) { + case 'useCanvasText': + _useCanvasText = enabled; + break; + } + } +} diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index e2004cda29286..298f79ec0c831 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -15,19 +15,11 @@ class EngineWindow extends ui.Window { } @override - double get devicePixelRatio { - if (_debugDevicePixelRatio != null) { - return _debugDevicePixelRatio; - } - - if (experimentalUseSkia) { - return browserDevicePixelRatio; - } else { - return 1.0; - } - } + double get devicePixelRatio => _debugDevicePixelRatio != null + ? _debugDevicePixelRatio + : browserDevicePixelRatio; - /// Returns device pixel ratio returns by browser. + /// Returns device pixel ratio returned by browser. static double get browserDevicePixelRatio { double ratio = html.window.devicePixelRatio; // Guard against WebOS returning 0. @@ -38,10 +30,7 @@ class EngineWindow extends ui.Window { /// /// This is useful in tests to emulate screens of different dimensions. void debugOverrideDevicePixelRatio(double value) { - assert(() { - _debugDevicePixelRatio = value; - return true; - }()); + _debugDevicePixelRatio = value; } double _debugDevicePixelRatio; @@ -121,19 +110,248 @@ class EngineWindow extends ui.Window { _browserHistory.locationStrategy = strategy; } + @override + ui.VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; + ui.VoidCallback _onTextScaleFactorChanged; + Zone _onTextScaleFactorChangedZone; + @override + set onTextScaleFactorChanged(ui.VoidCallback callback) { + _onTextScaleFactorChanged = callback; + _onTextScaleFactorChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnTextScaleFactorChanged() { + _invoke(_onTextScaleFactorChanged, _onTextScaleFactorChangedZone); + } + + @override + ui.VoidCallback get onPlatformBrightnessChanged => _onPlatformBrightnessChanged; + ui.VoidCallback _onPlatformBrightnessChanged; + Zone _onPlatformBrightnessChangedZone; + @override + set onPlatformBrightnessChanged(ui.VoidCallback callback) { + _onPlatformBrightnessChanged = callback; + _onPlatformBrightnessChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnPlatformBrightnessChanged() { + _invoke(_onPlatformBrightnessChanged, _onPlatformBrightnessChangedZone); + } + + @override + ui.VoidCallback get onMetricsChanged => _onMetricsChanged; + ui.VoidCallback _onMetricsChanged; + Zone _onMetricsChangedZone; + @override + set onMetricsChanged(ui.VoidCallback callback) { + _onMetricsChanged = callback; + _onMetricsChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnMetricsChanged() { + _invoke(_onMetricsChanged, _onMetricsChangedZone); + } + + @override + ui.VoidCallback get onLocaleChanged => _onLocaleChanged; + ui.VoidCallback _onLocaleChanged; + Zone _onLocaleChangedZone; + @override + set onLocaleChanged(ui.VoidCallback callback) { + _onLocaleChanged = callback; + _onLocaleChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnLocaleChanged() { + _invoke(_onLocaleChanged, _onLocaleChangedZone); + } + + @override + ui.FrameCallback get onBeginFrame => _onBeginFrame; + ui.FrameCallback _onBeginFrame; + Zone _onBeginFrameZone; + @override + set onBeginFrame(ui.FrameCallback callback) { + _onBeginFrame = callback; + _onBeginFrameZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnBeginFrame(Duration duration) { + _invoke1(_onBeginFrame, _onBeginFrameZone, duration); + } + + @override + ui.TimingsCallback get onReportTimings => _onReportTimings; + ui.TimingsCallback _onReportTimings; + Zone _onReportTimingsZone; + @override + set onReportTimings(ui.TimingsCallback callback) { + _onReportTimings = callback; + _onReportTimingsZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnReportTimings(List timings) { + _invoke1>(_onReportTimings, _onReportTimingsZone, timings); + } + + @override + ui.VoidCallback get onDrawFrame => _onDrawFrame; + ui.VoidCallback _onDrawFrame; + Zone _onDrawFrameZone; + @override + set onDrawFrame(ui.VoidCallback callback) { + _onDrawFrame = callback; + _onDrawFrameZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnDrawFrame() { + _invoke(_onDrawFrame, _onDrawFrameZone); + } + + @override + ui.PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket; + ui.PointerDataPacketCallback _onPointerDataPacket; + Zone _onPointerDataPacketZone; + @override + set onPointerDataPacket(ui.PointerDataPacketCallback callback) { + _onPointerDataPacket = callback; + _onPointerDataPacketZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnPointerDataPacket(ui.PointerDataPacket packet) { + _invoke1(_onPointerDataPacket, _onPointerDataPacketZone, packet); + } + + @override + ui.VoidCallback get onSemanticsEnabledChanged => _onSemanticsEnabledChanged; + ui.VoidCallback _onSemanticsEnabledChanged; + Zone _onSemanticsEnabledChangedZone; + @override + set onSemanticsEnabledChanged(ui.VoidCallback callback) { + _onSemanticsEnabledChanged = callback; + _onSemanticsEnabledChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnSemanticsEnabledChanged() { + _invoke(_onSemanticsEnabledChanged, _onSemanticsEnabledChangedZone); + } + + @override + ui.SemanticsActionCallback get onSemanticsAction => _onSemanticsAction; + ui.SemanticsActionCallback _onSemanticsAction; + Zone _onSemanticsActionZone; + @override + set onSemanticsAction(ui.SemanticsActionCallback callback) { + _onSemanticsAction = callback; + _onSemanticsActionZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnSemanticsAction(int id, ui.SemanticsAction action, ByteData args) { + _invoke3(_onSemanticsAction, + _onSemanticsActionZone, id, action, args); + } + + @override + ui.VoidCallback get onAccessibilityFeaturesChanged => _onAccessibilityFeaturesChanged; + ui.VoidCallback _onAccessibilityFeaturesChanged; + Zone _onAccessibilityFeaturesChangedZone; + @override + set onAccessibilityFeaturesChanged(ui.VoidCallback callback) { + _onAccessibilityFeaturesChanged = callback; + _onAccessibilityFeaturesChangedZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnAccessibilityFeaturesChanged() { + _invoke(_onAccessibilityFeaturesChanged, _onAccessibilityFeaturesChangedZone); + } + + @override + ui.PlatformMessageCallback get onPlatformMessage => _onPlatformMessage; + ui.PlatformMessageCallback _onPlatformMessage; + Zone _onPlatformMessageZone; + @override + set onPlatformMessage(ui.PlatformMessageCallback callback) { + _onPlatformMessage = callback; + _onPlatformMessageZone = Zone.current; + } + + /// Engine code should use this method instead of the callback directly. + /// Otherwise zones won't work properly. + void invokeOnPlatformMessage(String name, ByteData data, ui.PlatformMessageResponseCallback callback) { + _invoke3( + _onPlatformMessage, + _onPlatformMessageZone, + name, + data, + callback, + ); + } + @override void sendPlatformMessage( String name, ByteData data, ui.PlatformMessageResponseCallback callback, + ) { + _sendPlatformMessage(name, data, _zonedPlatformMessageResponseCallback(callback)); + } + + /// Wraps the given [callback] in another callback that ensures that the + /// original callback is called in the zone it was registered in. + static ui.PlatformMessageResponseCallback _zonedPlatformMessageResponseCallback(ui.PlatformMessageResponseCallback callback) { + if (callback == null) + return null; + + // Store the zone in which the callback is being registered. + final Zone registrationZone = Zone.current; + + return (ByteData data) { + registrationZone.runUnaryGuarded(callback, data); + }; + } + + void _sendPlatformMessage( + String name, + ByteData data, + ui.PlatformMessageResponseCallback callback, ) { // In widget tests we want to bypass processing of platform messages. if (assertionsEnabled && ui.debugEmulateFlutterTesterEnvironment) { return; } + if (_debugPrintPlatformMessages) { print('Sent platform message on channel: "$name"'); } + + if (assertionsEnabled && name == 'flutter/debug-echo') { + // Echoes back the data unchanged. Used for testing purpopses. + _replyToPlatformMessage(callback, data); + return; + } + switch (name) { case 'flutter/assets': assert(ui.webOnlyAssetManager != null); @@ -160,14 +378,17 @@ class EngineWindow extends ui.Window { case 'HapticFeedback.vibrate': final String type = decoded.arguments; domRenderer.vibrate(_getHapticFeedbackDuration(type)); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setApplicationSwitcherDescription': final Map arguments = decoded.arguments; domRenderer.setTitle(arguments['label']); domRenderer.setThemeColor(ui.Color(arguments['primaryColor'])); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); return; case 'SystemSound.play': // There are no default system sounds on web. + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); return; case 'Clipboard.setData': ClipboardMessageHandler().setDataMethodCall(decoded, callback); @@ -179,7 +400,14 @@ class EngineWindow extends ui.Window { break; case 'flutter/textinput': - textEditing.channel.handleTextInput(data); + textEditing.channel.handleTextInput(data, callback); + return; + + case 'flutter/web_test_e2e': + const MethodCodec codec = JSONMethodCodec(); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope( + _handleWebTestEnd2EndMessage(codec, data) + )); return; case 'flutter/platform_views': @@ -192,7 +420,9 @@ class EngineWindow extends ui.Window { case 'flutter/accessibility': // In widget tests we want to bypass processing of platform messages. - accessibilityAnnouncements.handleMessage(data); + final StandardMessageCodec codec = StandardMessageCodec(); + accessibilityAnnouncements.handleMessage(codec, data); + _replyToPlatformMessage(callback, codec.encodeMessage(true)); return; case 'flutter/navigation': @@ -204,11 +434,17 @@ class EngineWindow extends ui.Window { case 'routePushed': case 'routeReplaced': _browserHistory.setRouteName(message['routeName']); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); break; case 'routePopped': _browserHistory.setRouteName(message['previousRouteName']); + _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); break; } + // As soon as Flutter starts taking control of the app navigation, we + // should reset [_defaultRouteName] to "/" so it doesn't have any + // further effect after this point. + _defaultRouteName = '/'; return; } @@ -250,7 +486,9 @@ class EngineWindow extends ui.Window { ByteData data, ) { Future.delayed(Duration.zero).then((_) { - callback(data); + if (callback != null) { + callback(data); + } }); } @@ -265,7 +503,9 @@ class EngineWindow extends ui.Window { _platformBrightness = newPlatformBrightness; if (previousPlatformBrightness != _platformBrightness && - onPlatformBrightnessChanged != null) onPlatformBrightnessChanged(); + onPlatformBrightnessChanged != null) { + invokeOnPlatformBrightnessChanged(); + } } /// Reference to css media query that indicates the user theme preference on the web. @@ -315,6 +555,62 @@ class EngineWindow extends ui.Window { Rasterizer rasterizer = experimentalUseSkia ? Rasterizer(Surface()) : null; } +bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData data) { + final MethodCall decoded = codec.decodeMethodCall(data); + double ratio = double.parse(decoded.arguments); + switch(decoded.method) { + case 'setDevicePixelRatio': + window.debugOverrideDevicePixelRatio(ratio); + window.onMetricsChanged(); + return true; + } + return false; +} + +/// Invokes [callback] inside the given [zone]. +void _invoke(void callback(), Zone zone) { + if (callback == null) + return; + + assert(zone != null); + + if (identical(zone, Zone.current)) { + callback(); + } else { + zone.runGuarded(callback); + } +} + +/// Invokes [callback] inside the given [zone] passing it [arg]. +void _invoke1(void callback(A a), Zone zone, A arg) { + if (callback == null) + return; + + assert(zone != null); + + if (identical(zone, Zone.current)) { + callback(arg); + } else { + zone.runUnaryGuarded(callback, arg); + } +} + +/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3]. +void _invoke3(void callback(A1 a1, A2 a2, A3 a3), Zone zone, A1 arg1, A2 arg2, A3 arg3) { + if (callback == null) + return; + + assert(zone != null); + + if (identical(zone, Zone.current)) { + callback(arg1, arg2, arg3); + } else { + zone.runGuarded(() { + callback(arg1, arg2, arg3); + }); + } +} + /// The window singleton. /// /// `dart:ui` window delegates to this value. However, this value has a wider diff --git a/lib/web_ui/lib/src/ui/annotations.dart b/lib/web_ui/lib/src/ui/annotations.dart new file mode 100644 index 0000000000000..10b9ef2694b1f --- /dev/null +++ b/lib/web_ui/lib/src/ui/annotations.dart @@ -0,0 +1,46 @@ +// 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. + +// TODO(dnfield): Remove unused_import ignores when https://github.com/dart-lang/sdk/issues/35164 is resolved. + +// @dart = 2.6 +part of ui; + +// TODO(dnfield): Update this if/when we default this to on in the tool, +// see: https://github.com/flutter/flutter/issues/52759 +/// Annotation used by Flutter's Dart compiler to indicate that an +/// [Object.toString] override should not be replaced with a supercall. +/// +/// Since `dart:ui` and `package:flutter` override `toString` purely for +/// debugging purposes, the frontend compiler is instructed to replace all +/// `toString` bodies with `return super.toString()` during compilation. This +/// significantly reduces release code size, and would make it impossible to +/// implement a meaningful override of `toString` for release mode without +/// disabling the feature and losing the size savings. If a package uses this +/// feature and has some unavoidable need to keep the `toString` implementation +/// for a specific class, applying this annotation will direct the compiler +/// to leave the method body as-is. +/// +/// For example, in the following class the `toString` method will remain as +/// `return _buffer.toString();`, even if the `--delete-tostring-package-uri` +/// option would otherwise apply and replace it with `return super.toString()`. +/// +/// ```dart +/// class MyStringBuffer { +/// StringBuffer _buffer = StringBuffer(); +/// +/// // ... +/// +/// @keepToString +/// @override +/// String toString() { +/// return _buffer.toString(); +/// } +/// } +/// ``` +const _KeepToString keepToString = _KeepToString(); + +class _KeepToString { + const _KeepToString(); +} diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index 9359f0487e00a..c27e513fc4b08 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -963,7 +963,9 @@ class Canvas { } void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint) { - if (vertices == null) return; + if (vertices == null) { + return; + } //assert(vertices != null); // vertices is checked on the engine side assert(paint != null); assert(blendMode != null); diff --git a/lib/web_ui/lib/src/ui/compositing.dart b/lib/web_ui/lib/src/ui/compositing.dart index f5c5a56baad66..0bdd3441a4499 100644 --- a/lib/web_ui/lib/src/ui/compositing.dart +++ b/lib/web_ui/lib/src/ui/compositing.dart @@ -289,8 +289,8 @@ abstract class SceneBuilder { /// controls where the statistics are displayed. /// /// enabledOptions is a bit field with the following bits defined: - /// - 0x01: displayRasterizerStatistics - show GPU thread frame time - /// - 0x02: visualizeRasterizerStatistics - graph GPU thread frame times + /// - 0x01: displayRasterizerStatistics - show raster thread frame time + /// - 0x02: visualizeRasterizerStatistics - graph raster thread frame times /// - 0x04: displayEngineStatistics - show UI thread frame time /// - 0x08: visualizeEngineStatistics - graph UI thread frame times /// Set enabledOptions to 0x0F to enable all the currently defined features. @@ -298,7 +298,7 @@ abstract class SceneBuilder { /// The "UI thread" is the thread that includes all the execution of /// the main Dart isolate (the isolate that can call /// [Window.render]). The UI thread frame time is the total time - /// spent executing the [Window.onBeginFrame] callback. The "GPU + /// spent executing the [Window.onBeginFrame] callback. The "raster /// thread" is the thread (running on the CPU) that subsequently /// processes the [Scene] provided by the Dart code to turn it into /// GPU commands and send it to the GPU. diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 3eced5b3c6ebe..b08ef9dafab40 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -899,9 +899,8 @@ enum Clip { abstract class Paint { /// Constructs an empty [Paint] object with all fields initialized to /// their defaults. - factory Paint() => engine.experimentalUseSkia - ? engine.SkPaint() - : engine.SurfacePaint(); + factory Paint() => + engine.experimentalUseSkia ? engine.SkPaint() : engine.SurfacePaint(); /// Whether to dither the output when drawing images. /// @@ -1070,8 +1069,6 @@ abstract class Shader { /// There are several types of gradients, represented by the various /// constructors on this class. abstract class Gradient extends Shader { - Gradient._() : super._(); - /// Creates a linear gradient from `from` to `to`. /// /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 @@ -1604,13 +1601,17 @@ String _instantiateImageCodec( return null; } -Future webOnlyInstantiateImageCodecFromUrl(Uri uri) { +Future webOnlyInstantiateImageCodecFromUrl(Uri uri, + {engine.WebOnlyImageCodecChunkCallback chunkCallback}) { return engine.futurize((engine.Callback callback) => - _instantiateImageCodecFromUrl(uri, callback)); + _instantiateImageCodecFromUrl(uri, chunkCallback, callback)); } -String _instantiateImageCodecFromUrl(Uri uri, engine.Callback callback) { - callback(engine.HtmlCodec(uri.toString())); +String _instantiateImageCodecFromUrl( + Uri uri, + engine.WebOnlyImageCodecChunkCallback chunkCallback, + engine.Callback callback) { + callback(engine.HtmlCodec(uri.toString(), chunkCallback: chunkCallback)); return null; } diff --git a/lib/web_ui/lib/src/ui/test_embedding.dart b/lib/web_ui/lib/src/ui/test_embedding.dart index 5952b19ed8c2f..471876706c0f4 100644 --- a/lib/web_ui/lib/src/ui/test_embedding.dart +++ b/lib/web_ui/lib/src/ui/test_embedding.dart @@ -29,7 +29,7 @@ Future ensureTestPlatformInitializedThenRunTest( /// are available. Future _platformInitializedFuture; -/// Initializes domRenderer with specific devicePixelRation and physicalSize. +/// Initializes domRenderer with specific devicePixelRatio and physicalSize. Future webOnlyInitializeTestDomRenderer({double devicePixelRatio = 3.0}) { // Force-initialize DomRenderer so it doesn't overwrite test pixel ratio. engine.domRenderer; diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index aeba957a8da7b..18aec485487ff 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -866,14 +866,6 @@ class TextBox { this.direction, ); - TextBox._( - this.left, - this.top, - this.right, - this.bottom, - int directionIndex, - ) : direction = TextDirection.values[directionIndex]; - /// The left edge of the text box, irrespective of direction. /// /// To get the leading edge (which may depend on the [direction]), consider [start]. @@ -1113,8 +1105,12 @@ class TextRange { @override bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! TextRange) return false; + if (identical(this, other)) { + return true; + } + if (other is! TextRange) { + return false; + } final TextRange typedOther = other; return typedOther.start == start && typedOther.end == end; } @@ -1590,22 +1586,11 @@ abstract class ParagraphBuilder { Future loadFontFromList(Uint8List list, {String fontFamily}) { if (engine.experimentalUseSkia) { return engine.skiaFontCollection.loadFontFromList(list, fontFamily: fontFamily).then( - (_) => _sendFontChangeMessage() + (_) => engine.sendFontChangeMessage() ); } else { return _fontCollection.loadFontFromList(list, fontFamily: fontFamily).then( - (_) => _sendFontChangeMessage() + (_) => engine.sendFontChangeMessage() ); } } - -final ByteData _fontChangeMessage = engine.JSONMessageCodec().encodeMessage({'type': 'fontsChange'}); - -FutureOr _sendFontChangeMessage() async { - if (window.onPlatformMessage != null) - window.onPlatformMessage( - 'flutter/system', - _fontChangeMessage, - (_) {}, - ); -} diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index f19432c4e0832..58c2d672a13eb 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -485,8 +485,12 @@ class Locale { String _rawToString(String separator) { final StringBuffer out = StringBuffer(languageCode); - if (scriptCode != null) out.write('$separator$scriptCode'); - if (_countryCode != null) out.write('$separator$countryCode'); + if (scriptCode != null) { + out.write('$separator$scriptCode'); + } + if (_countryCode != null) { + out.write('$separator$countryCode'); + } return out.toString(); } } @@ -621,11 +625,8 @@ abstract class Window { /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this callback is invoked. - VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; - VoidCallback _onTextScaleFactorChanged; - set onTextScaleFactorChanged(VoidCallback callback) { - _onTextScaleFactorChanged = callback; - } + VoidCallback get onTextScaleFactorChanged; + set onTextScaleFactorChanged(VoidCallback callback); /// The setting indicating the current brightness mode of the host platform. Brightness get platformBrightness; @@ -639,11 +640,8 @@ abstract class Window { /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this callback is invoked. - VoidCallback get onPlatformBrightnessChanged => _onPlatformBrightnessChanged; - VoidCallback _onPlatformBrightnessChanged; - set onPlatformBrightnessChanged(VoidCallback callback) { - _onPlatformBrightnessChanged = callback; - } + VoidCallback get onPlatformBrightnessChanged; + set onPlatformBrightnessChanged(VoidCallback callback); /// A callback that is invoked whenever the [devicePixelRatio], /// [physicalSize], [padding], or [viewInsets] values change, for example @@ -661,11 +659,8 @@ abstract class Window { /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// register for notifications when this is called. /// * [MediaQuery.of], a simpler mechanism for the same. - VoidCallback get onMetricsChanged => _onMetricsChanged; - VoidCallback _onMetricsChanged; - set onMetricsChanged(VoidCallback callback) { - _onMetricsChanged = callback; - } + VoidCallback get onMetricsChanged; + set onMetricsChanged(VoidCallback callback); static const _enUS = const Locale('en', 'US'); @@ -713,11 +708,8 @@ abstract class Window { /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this callback is invoked. - VoidCallback get onLocaleChanged => _onLocaleChanged; - VoidCallback _onLocaleChanged; - set onLocaleChanged(VoidCallback callback) { - _onLocaleChanged = callback; - } + VoidCallback get onLocaleChanged; + set onLocaleChanged(VoidCallback callback); /// Requests that, at the next appropriate opportunity, the [onBeginFrame] /// and [onDrawFrame] callbacks be invoked. @@ -753,11 +745,8 @@ abstract class Window { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - FrameCallback get onBeginFrame => _onBeginFrame; - FrameCallback _onBeginFrame; - set onBeginFrame(FrameCallback callback) { - _onBeginFrame = callback; - } + FrameCallback get onBeginFrame; + set onBeginFrame(FrameCallback callback); /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. @@ -775,12 +764,8 @@ abstract class Window { /// decrease the overhead (as this is available in the release mode). The /// timing of any frame will be sent within about 1 second even if there are /// no later frames to batch. - TimingsCallback get onReportTimings => _onReportTimings; - TimingsCallback _onReportTimings; - Zone _onReportTimingsZone; // ignore: unused_field - set onReportTimings(TimingsCallback callback) { - _onReportTimings = callback; - } + TimingsCallback get onReportTimings; + set onReportTimings(TimingsCallback callback); /// A callback that is invoked for each frame after [onBeginFrame] has /// completed and after the microtask queue has been drained. This can be @@ -796,11 +781,8 @@ abstract class Window { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - VoidCallback get onDrawFrame => _onDrawFrame; - VoidCallback _onDrawFrame; - set onDrawFrame(VoidCallback callback) { - _onDrawFrame = callback; - } + VoidCallback get onDrawFrame; + set onDrawFrame(VoidCallback callback); /// A callback that is invoked when pointer data is available. /// @@ -811,11 +793,8 @@ abstract class Window { /// /// * [GestureBinding], the Flutter framework class which manages pointer /// events. - PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket; - PointerDataPacketCallback _onPointerDataPacket; - set onPointerDataPacket(PointerDataPacketCallback callback) { - _onPointerDataPacket = callback; - } + PointerDataPacketCallback get onPointerDataPacket; + set onPointerDataPacket(PointerDataPacketCallback callback); /// The route or path that the embedder requested when the application was /// launched. @@ -864,11 +843,8 @@ abstract class Window { /// /// The framework invokes this callback in the same zone in which the /// callback was set. - VoidCallback get onSemanticsEnabledChanged => _onSemanticsEnabledChanged; - VoidCallback _onSemanticsEnabledChanged; - set onSemanticsEnabledChanged(VoidCallback callback) { - _onSemanticsEnabledChanged = callback; - } + VoidCallback get onSemanticsEnabledChanged; + set onSemanticsEnabledChanged(VoidCallback callback); /// A callback that is invoked whenever the user requests an action to be /// performed. @@ -878,22 +854,15 @@ abstract class Window { /// /// The framework invokes this callback in the same zone in which the /// callback was set. - SemanticsActionCallback get onSemanticsAction => _onSemanticsAction; - SemanticsActionCallback _onSemanticsAction; - set onSemanticsAction(SemanticsActionCallback callback) { - _onSemanticsAction = callback; - } + SemanticsActionCallback get onSemanticsAction; + set onSemanticsAction(SemanticsActionCallback callback); /// A callback that is invoked when the value of [accessibilityFlags] changes. /// /// The framework invokes this callback in the same zone in which the /// callback was set. - VoidCallback get onAccessibilityFeaturesChanged => - _onAccessibilityFeaturesChanged; - VoidCallback _onAccessibilityFeaturesChanged; - set onAccessibilityFeaturesChanged(VoidCallback callback) { - _onAccessibilityFeaturesChanged = callback; - } + VoidCallback get onAccessibilityFeaturesChanged; + set onAccessibilityFeaturesChanged(VoidCallback callback); /// Called whenever this window receives a message from a platform-specific /// plugin. @@ -908,11 +877,8 @@ abstract class Window { /// /// The framework invokes this callback in the same zone in which the /// callback was set. - PlatformMessageCallback get onPlatformMessage => _onPlatformMessage; - PlatformMessageCallback _onPlatformMessage; - set onPlatformMessage(PlatformMessageCallback callback) { - _onPlatformMessage = callback; - } + PlatformMessageCallback get onPlatformMessage; + set onPlatformMessage(PlatformMessageCallback callback); /// Change the retained semantics data about this window. /// @@ -1144,12 +1110,12 @@ enum FramePhase { /// See also [FrameTiming.buildDuration]. buildFinish, - /// When the GPU thread starts rasterizing a frame. + /// When the raster thread starts rasterizing a frame. /// /// See also [FrameTiming.rasterDuration]. rasterStart, - /// When the GPU thread finishes rasterizing a frame. + /// When the raster thread finishes rasterizing a frame. /// /// See also [FrameTiming.rasterDuration]. rasterFinish, @@ -1201,7 +1167,7 @@ class FrameTiming { _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); - /// The duration to rasterize the frame on the GPU thread. + /// The duration to rasterize the frame on the raster thread. /// /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} /// {@macro dart.ui.FrameTiming.fps_milliseconds} diff --git a/lib/web_ui/lib/ui.dart b/lib/web_ui/lib/ui.dart index 4acb5e246bfd8..8565c42d034e3 100644 --- a/lib/web_ui/lib/ui.dart +++ b/lib/web_ui/lib/ui.dart @@ -24,6 +24,7 @@ export 'src/engine.dart' webOnlySetPluginHandler, webOnlyInitializeEngine; +part 'src/ui/annotations.dart'; part 'src/ui/canvas.dart'; part 'src/ui/channel_buffers.dart'; part 'src/ui/compositing.dart'; diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 12e7d8d8f7d7a..75743250398be 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -21,3 +21,8 @@ dev_dependencies: watcher: 0.9.7+12 web_engine_tester: path: ../../web_sdk/web_engine_tester + web_driver_installer: + git: + url: git://github.com/flutter/web_installers.git + path: packages/web_drivers/ + ref: 90c69a79b2764c93875dc8ed4f4932c27a6f3a86 diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 363204a120d6e..ce9d1499caca2 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -11,6 +11,11 @@ import 'package:test/test.dart'; import 'mock_engine_canvas.dart'; void main() { + setUpAll(() { + WebExperiments.ensureInitialized(); + Profiler.ensureInitialized(); + }); + group('EngineCanvas', () { MockEngineCanvas mockCanvas; ui.Paragraph paragraph; diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 01e67c388e92d..494b565ddd4c4 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 -@TestOn('vm && linux') +@TestOn('!safari') // TODO(nurhan): https://github.com/flutter/flutter/issues/51169 import 'dart:async'; @@ -171,7 +171,7 @@ void main() { await _strategy.simulateUserTypingUrl('/page3'); // This delay is necessary to wait for [BrowserHistory] because it // performs a `back` operation which results in a new event loop. - await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); // 1. The engine sends a `pushRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -212,7 +212,7 @@ void main() { await _strategy.simulateUserTypingUrl('/unknown'); // This delay is necessary to wait for [BrowserHistory] because it // performs a `back` operation which results in a new event loop. - await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); // 1. The engine sends a `pushRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); diff --git a/lib/web_ui/test/engine/image/html_image_codec_test.dart b/lib/web_ui/test/engine/image/html_image_codec_test.dart new file mode 100644 index 0000000000000..38cd5602d32f4 --- /dev/null +++ b/lib/web_ui/test/engine/image/html_image_codec_test.dart @@ -0,0 +1,29 @@ +// 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. + +// @dart = 2.6 +import 'package:test/test.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:ui/src/engine.dart'; + +Future main() async { + await ui.webOnlyInitializeTestDomRenderer(); + group('HtmCodec', () { + test('loads sample image', () async { + final HtmlCodec codec = HtmlCodec('sample_image1.png'); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + expect(frameInfo.image, isNotNull); + expect(frameInfo.image.width, 100); + }); + test('provides image loading progress', () async { + StringBuffer buffer = new StringBuffer(); + final HtmlCodec codec = HtmlCodec('sample_image1.png', + chunkCallback: (int loaded, int total) { + buffer.write('$loaded/$total,'); + }); + await codec.getNextFrame(); + expect(buffer.toString(), '0/100,100/100,'); + }); + }); +} diff --git a/lib/web_ui/test/engine/image/sample_image1.png b/lib/web_ui/test/engine/image/sample_image1.png new file mode 100644 index 0000000000000..0d1393b805213 Binary files /dev/null and b/lib/web_ui/test/engine/image/sample_image1.png differ diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index a23d559bb54b4..3f8ed22b0968c 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -414,6 +414,75 @@ void main() { }, ); + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'correctly detects events on the semantics placeholder', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + final html.Element semanticsPlaceholder = + html.Element.tag('flt-semantics-placeholder'); + glassPane.append(semanticsPlaceholder); + + // Press on the semantics placeholder. + semanticsPlaceholder.dispatchEvent(context.primaryDown( + clientX: 10.0, + clientY: 10.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(2)); + expect(packets[0].data[0].change, equals(ui.PointerChange.add)); + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].physicalX, equals(10.0)); + expect(packets[0].data[1].physicalY, equals(10.0)); + packets.clear(); + + // Drag on the semantics placeholder. + semanticsPlaceholder.dispatchEvent(context.primaryMove( + clientX: 12.0, + clientY: 10.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].physicalX, equals(12.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + packets.clear(); + + // Keep dragging. + semanticsPlaceholder.dispatchEvent(context.primaryMove( + clientX: 15.0, + clientY: 10.0, + )); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].physicalX, equals(15.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + packets.clear(); + + // Release the pointer on the semantics placeholder. + html.window.dispatchEvent(context.primaryUp( + clientX: 100.0, + clientY: 200.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(2)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].physicalX, equals(100.0)); + expect(packets[0].data[0].physicalY, equals(200.0)); + expect(packets[0].data[1].change, equals(ui.PointerChange.up)); + expect(packets[0].data[1].physicalX, equals(100.0)); + expect(packets[0].data[1].physicalY, equals(200.0)); + packets.clear(); + + semanticsPlaceholder.remove(); + }, + ); + // BUTTONED ADAPTERS _testEach<_ButtonedEventMixin>( @@ -1446,6 +1515,67 @@ void main() { }, ); + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'correctly detects up event outside of glasspane', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + // This can happen when the up event occurs while the mouse is outside the + // browser window. + + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Press and drag around. + glassPane.dispatchEvent(context.primaryDown( + clientX: 10.0, + clientY: 10.0, + )); + glassPane.dispatchEvent(context.primaryMove( + clientX: 12.0, + clientY: 10.0, + )); + glassPane.dispatchEvent(context.primaryMove( + clientX: 15.0, + clientY: 10.0, + )); + glassPane.dispatchEvent(context.primaryMove( + clientX: 20.0, + clientY: 10.0, + )); + packets.clear(); + + // Move outside the glasspane. + html.window.dispatchEvent(context.primaryMove( + clientX: 900.0, + clientY: 1900.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].physicalX, equals(900.0)); + expect(packets[0].data[0].physicalY, equals(1900.0)); + packets.clear(); + + // Release outside the glasspane. + html.window.dispatchEvent(context.primaryUp( + clientX: 1000.0, + clientY: 2000.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(2)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].physicalX, equals(1000.0)); + expect(packets[0].data[0].physicalY, equals(2000.0)); + expect(packets[0].data[1].change, equals(ui.PointerChange.up)); + expect(packets[0].data[1].physicalX, equals(1000.0)); + expect(packets[0].data[1].physicalY, equals(2000.0)); + packets.clear(); + }, + ); + // MULTIPOINTER ADAPTERS _testEach<_MultiPointerEventMixin>( @@ -1970,7 +2100,7 @@ class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin double clientX, double clientY, }) { - return html.Touch({ + return html.Touch({ 'identifier': identifier, 'clientX': clientX, 'clientY': clientY, @@ -1979,7 +2109,7 @@ class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin } html.TouchEvent _createTouchEvent(String eventType, List<_TouchDetails> touches) { - return html.TouchEvent(eventType, { + return html.TouchEvent(eventType, { 'changedTouches': touches.map( (_TouchDetails details) => _createTouch( @@ -2138,7 +2268,7 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i } html.Event _downWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { - return html.PointerEvent('pointerdown', { + return html.PointerEvent('pointerdown', { 'pointerId': pointer, 'button': button, 'buttons': buttons, @@ -2173,7 +2303,7 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i } html.Event _moveWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { - return html.PointerEvent('pointermove', { + return html.PointerEvent('pointermove', { 'pointerId': pointer, 'button': button, 'buttons': buttons, @@ -2206,7 +2336,7 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i } html.Event _upWithFullDetails({double clientX, double clientY, int button, int pointer, String pointerType}) { - return html.PointerEvent('pointerup', { + return html.PointerEvent('pointerup', { 'pointerId': pointer, 'button': button, 'buttons': 0, @@ -2218,7 +2348,7 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i @override List multiTouchCancel(List<_TouchDetails> touches) { - return touches.map((_TouchDetails details) => html.PointerEvent('pointercancel', { + return touches.map((_TouchDetails details) => html.PointerEvent('pointercancel', { 'pointerId': details.pointer, 'button': 0, 'buttons': 0, diff --git a/lib/web_ui/test/engine/profiler_test.dart b/lib/web_ui/test/engine/profiler_test.dart new file mode 100644 index 0000000000000..04d580d2fa969 --- /dev/null +++ b/lib/web_ui/test/engine/profiler_test.dart @@ -0,0 +1,99 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:js_util' as js_util; + +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +void main() { + setUp(() { + Profiler.isBenchmarkMode = true; + Profiler.ensureInitialized(); + }); + + tearDown(() { + jsOnBenchmark(null); + Profiler.isBenchmarkMode = false; + }); + + test('works when there is no listener', () { + expect(() => Profiler.instance.benchmark('foo', 123), returnsNormally); + }); + + test('can listen to benchmarks', () { + final List data = []; + jsOnBenchmark((String name, num value) { + data.add(BenchmarkDatapoint(name, value)); + }); + + Profiler.instance.benchmark('foo', 123); + expect(data, [BenchmarkDatapoint('foo', 123)]); + data.clear(); + + Profiler.instance.benchmark('bar', 0.0125); + expect(data, [BenchmarkDatapoint('bar', 0.0125)]); + data.clear(); + + // Remove listener and make sure nothing breaks and the data isn't being + // sent to the old callback anymore. + jsOnBenchmark(null); + expect(() => Profiler.instance.benchmark('baz', 99.999), returnsNormally); + expect(data, isEmpty); + }); + + test('throws on wrong listener type', () { + final List data = []; + + // Wrong callback signature. + jsOnBenchmark((num value) { + data.add(BenchmarkDatapoint('bad', value)); + }); + expect( + () => Profiler.instance.benchmark('foo', 123), + throwsA(isA()), + ); + expect(data, isEmpty); + + // Not even a callback. + jsOnBenchmark('string'); + expect( + () => Profiler.instance.benchmark('foo', 123), + throwsA(isA()), + ); + }); +} + +class BenchmarkDatapoint { + BenchmarkDatapoint(this.name, this.value); + + final String name; + final num value; + + @override + int get hashCode => hashValues(name, value); + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return name == other.name && value == other.value; + } + + @override + String toString() { + return '$runtimeType("$name", $value)'; + } +} + +void jsOnBenchmark(dynamic listener) { + js_util.setProperty(html.window, '_flutter_internal_on_benchmark', listener); +} diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index 6e375bfc9ac6d..2757ba96fd4a4 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -27,7 +27,7 @@ void main() { underTest.drawDRRect(rrect, rrect.deflate(1), somePaint); underTest.apply(mockCanvas); - _expectDrawCall(mockCanvas, { + _expectDrawCall(mockCanvas, { 'outer': rrect, 'inner': rrect.deflate(1), 'paint': somePaint.paintData, @@ -75,7 +75,7 @@ void main() { underTest.apply(mockCanvas); // Expect to draw, even when inner has negative radii (which get ignored by canvas) - _expectDrawCall(mockCanvas, { + _expectDrawCall(mockCanvas, { 'outer': outer, 'inner': inner, 'paint': somePaint.paintData, @@ -91,7 +91,7 @@ void main() { underTest.drawDRRect(outer, inner, somePaint); underTest.apply(mockCanvas); - _expectDrawCall(mockCanvas, { + _expectDrawCall(mockCanvas, { 'outer': outer, 'inner': inner, 'paint': somePaint.paintData, diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index 8c572563c38db..767d4742ad0ea 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -33,7 +33,8 @@ void main() { // Initially there is no accessibility-element expect(document.getElementById('accessibility-element'), isNull); - accessibilityAnnouncements.handleMessage(codec.encodeMessage(testInput)); + accessibilityAnnouncements.handleMessage(codec, + codec.encodeMessage(testInput)); expect( document.getElementById('accessibility-element'), isNotNull, diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart new file mode 100644 index 0000000000000..034cbba04b945 --- /dev/null +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -0,0 +1,80 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:js_util' as js_util; + +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + +void main() { + setUp(() { + WebExperiments.ensureInitialized(); + }); + + tearDown(() { + WebExperiments.instance.reset(); + }); + + test('default web experiment values', () { + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('can turn on/off web experiments', () { + WebExperiments.instance.updateExperiment('useCanvasText', true); + expect(WebExperiments.instance.useCanvasText, true); + + WebExperiments.instance.updateExperiment('useCanvasText', false); + expect(WebExperiments.instance.useCanvasText, false); + + WebExperiments.instance.updateExperiment('useCanvasText', null); + // Goes back to default value. + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('ignores unknown experiments', () { + expect(WebExperiments.instance.useCanvasText, false); + WebExperiments.instance.updateExperiment('foobarbazqux', true); + expect(WebExperiments.instance.useCanvasText, false); + WebExperiments.instance.updateExperiment('foobarbazqux', false); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('can reset web experiments', () { + WebExperiments.instance.updateExperiment('useCanvasText', true); + WebExperiments.instance.reset(); + expect(WebExperiments.instance.useCanvasText, false); + + WebExperiments.instance.updateExperiment('useCanvasText', true); + WebExperiments.instance.updateExperiment('foobarbazqux', true); + WebExperiments.instance.reset(); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('js interop also works', () { + expect(WebExperiments.instance.useCanvasText, false); + + expect(() => jsUpdateExperiment('useCanvasText', true), returnsNormally); + expect(WebExperiments.instance.useCanvasText, true); + + expect(() => jsUpdateExperiment('useCanvasText', null), returnsNormally); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('js interop throws on wrong type', () { + expect(() => jsUpdateExperiment(123, true), throwsA(anything)); + expect(() => jsUpdateExperiment('foo', 123), throwsA(anything)); + expect(() => jsUpdateExperiment('foo', 'bar'), throwsA(anything)); + expect(() => jsUpdateExperiment(false, 'foo'), throwsA(anything)); + }); +} + +void jsUpdateExperiment(dynamic name, dynamic enabled) { + js_util.callMethod( + html.window, + '_flutter_internal_update_experiment', + [name, enabled], + ); +} diff --git a/lib/web_ui/test/engine/window_test.dart b/lib/web_ui/test/engine/window_test.dart new file mode 100644 index 0000000000000..4dd297627285e --- /dev/null +++ b/lib/web_ui/test/engine/window_test.dart @@ -0,0 +1,225 @@ +// 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. + +// @dart = 2.6 +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:test/test.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:ui/src/engine.dart'; + +void main() { + test('onTextScaleFactorChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onTextScaleFactorChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onTextScaleFactorChanged, same(callback)); + }); + + window.invokeOnTextScaleFactorChanged(); + }); + + test('onPlatformBrightnessChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onPlatformBrightnessChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onPlatformBrightnessChanged, same(callback)); + }); + + window.invokeOnPlatformBrightnessChanged(); + }); + + test('onMetricsChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onMetricsChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onMetricsChanged, same(callback)); + }); + + window.invokeOnMetricsChanged(); + }); + + test('onLocaleChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onLocaleChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onLocaleChanged, same(callback)); + }); + + window.invokeOnLocaleChanged(); + }); + + test('onBeginFrame preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.FrameCallback callback = (_) { + expect(Zone.current, innerZone); + }; + window.onBeginFrame = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onBeginFrame, same(callback)); + }); + + window.invokeOnBeginFrame(null); + }); + + test('onReportTimings preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.TimingsCallback callback = (_) { + expect(Zone.current, innerZone); + }; + window.onReportTimings = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onReportTimings, same(callback)); + }); + + window.invokeOnReportTimings(null); + }); + + test('onDrawFrame preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onDrawFrame = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onDrawFrame, same(callback)); + }); + + window.invokeOnDrawFrame(); + }); + + test('onPointerDataPacket preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.PointerDataPacketCallback callback = (_) { + expect(Zone.current, innerZone); + }; + window.onPointerDataPacket = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onPointerDataPacket, same(callback)); + }); + + window.invokeOnPointerDataPacket(null); + }); + + test('onSemanticsEnabledChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onSemanticsEnabledChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onSemanticsEnabledChanged, same(callback)); + }); + + window.invokeOnSemanticsEnabledChanged(); + }); + + test('onSemanticsAction preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.SemanticsActionCallback callback = (_, __, ___) { + expect(Zone.current, innerZone); + }; + window.onSemanticsAction = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onSemanticsAction, same(callback)); + }); + + window.invokeOnSemanticsAction(null, null, null); + }); + + test('onAccessibilityFeaturesChanged preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.VoidCallback callback = () { + expect(Zone.current, innerZone); + }; + window.onAccessibilityFeaturesChanged = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onAccessibilityFeaturesChanged, same(callback)); + }); + + window.invokeOnAccessibilityFeaturesChanged(); + }); + + test('onPlatformMessage preserves the zone', () { + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ui.PlatformMessageCallback callback = (_, __, ___) { + expect(Zone.current, innerZone); + }; + window.onPlatformMessage = callback; + + // Test that the getter returns the exact same callback, e.g. it doesn't wrap it. + expect(window.onPlatformMessage, same(callback)); + }); + + window.invokeOnPlatformMessage(null, null, null); + }); + + test('sendPlatformMessage preserves the zone', () async { + final Completer completer = Completer(); + final Zone innerZone = Zone.current.fork(); + + innerZone.runGuarded(() { + final ByteData inputData = ByteData(4); + inputData.setUint32(0, 42); + window.sendPlatformMessage( + 'flutter/debug-echo', + inputData, + (outputData) { + expect(Zone.current, innerZone); + completer.complete(); + }, + ); + }); + + await completer.future; + }); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart index ae316b86992ee..694274bfe516e 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart @@ -19,7 +19,8 @@ void main() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, - {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { + {Rect region = const Rect.fromLTWH(0, 0, 500, 500), + double maxDiffRatePercent = 0.0}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.apply(engineCanvas); @@ -29,7 +30,7 @@ void main() async { try { sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: 0.0); + await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -76,7 +77,8 @@ void main() async { ..style = PaintingStyle.fill ..color = const Color.fromARGB(128, 255, 0, 0)); rc.restore(); - await _checkScreenshot(rc, 'canvas_blend_circle_diff_color'); + await _checkScreenshot(rc, 'canvas_blend_circle_diff_color', + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); }); test('Blend circle and text with multiply', () async { @@ -112,7 +114,8 @@ void main() async { rc.drawImage(createTestImage(), Offset(135.0, 130.0), Paint()..blendMode = BlendMode.multiply); rc.restore(); - await _checkScreenshot(rc, 'canvas_blend_image_multiply'); + await _checkScreenshot(rc, 'canvas_blend_image_multiply', + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); }); } @@ -132,6 +135,6 @@ HtmlImage createTestImage() { ctx.fillRect(66, 0, 33, 50); ctx.fill(); html.ImageElement imageElement = html.ImageElement(); - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart index 81f3fc8b8eadf..316b6b3467fde 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart @@ -101,6 +101,6 @@ engine.HtmlImage createTestImage({int width = 200, int height = 150}) { ctx.fillRect(2 * width / 3, 0, width / 3, height); ctx.fill(); html.ImageElement imageElement = html.ImageElement(); - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return engine.HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart index d6e3ee13e9711..f0d2f15a08a77 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart @@ -4,7 +4,6 @@ // @dart = 2.6 import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart index 1e86fefdb4cbb..dcaf6c93b59bd 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -7,7 +7,7 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:js_util' as js_util; -import 'package:ui/ui.dart' hide TextStyle; +import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; import 'package:test/test.dart'; @@ -22,7 +22,8 @@ void main() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, - {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { + {Rect region = const Rect.fromLTWH(0, 0, 500, 500), + double maxDiffRatePercent = 0.0}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.apply(engineCanvas); @@ -32,7 +33,8 @@ void main() async { try { sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: 0.0); + await matchGoldenFile('$fileName.png', + region: region, maxDiffRatePercent: maxDiffRatePercent); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -310,6 +312,39 @@ void main() async { rc.restore(); await _checkScreenshot(rc, 'draw_circle_on_image_clip_path'); }); + + // Regression test for https://github.com/flutter/flutter/issues/53078 + // Verified that Text+Image+Text+Rect+Text composites correctly. + // Yellow text should be behind image and rectangle. + // Cyan text should be above everything. + test('Paints text above and below image', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + Image testImage = createTestImage(); + double testWidth = testImage.width.toDouble(); + double testHeight = testImage.height.toDouble(); + final Paragraph paragraph1 = createTestParagraph( + 'should be below...............', + color: Color(0xFFFFFF40)); + paragraph1.layout(const ParagraphConstraints(width: 400.0)); + rc.drawParagraph(paragraph1, const Offset(20, 100)); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 100, 200, 200), Paint()); + rc.drawRect( + Rect.fromLTWH(50, 50, 100, 200), + Paint() + ..strokeWidth = 3 + ..color = Color(0xA0000000)); + final Paragraph paragraph2 = createTestParagraph( + 'Should be above...............', + color: Color(0xFF00FFFF)); + paragraph2.layout(const ParagraphConstraints(width: 400.0)); + rc.drawParagraph(paragraph2, const Offset(20, 150)); + rc.restore(); + await _checkScreenshot(rc, 'draw_text_composite_order_below', + maxDiffRatePercent: 1.0); + }); } HtmlImage createTestImage({int width = 100, int height = 50}) { @@ -326,6 +361,19 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { ctx.fillRect(66, 0, 33, 50); ctx.fill(); html.ImageElement imageElement = html.ImageElement(); - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } + +Paragraph createTestParagraph(String text, + {Color color = const Color(0xFF000000)}) { + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Ahem', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 14.0, + )); + builder.pushStyle(TextStyle(color: color)); + builder.addText(text); + return builder.build(); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart new file mode 100644 index 0000000000000..de4c52cebb1d2 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_picture_test.dart @@ -0,0 +1,122 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +final Rect region = Rect.fromLTWH(0, 0, 500, 100); + +void main() async { + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('draw growing picture across frames', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + builder.pushClipRect( + const Rect.fromLTRB(0, 0, 100, 100), + ); + + _drawTestPicture(builder, 100, false); + builder.pop(); + + html.Element elm1 = builder.build().webOnlyRootElement; + html.document.body.append(elm1); + + // Now draw picture again but at larger size. + final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder(); + builder2.pushClipRect( + const Rect.fromLTRB(0, 0, 100, 100), + ); + // Now draw the picture at original target size, which will use a + // different code path that should normally not have width/height set + // on image element. + _drawTestPicture(builder2, 20, false); + builder2.pop(); + + elm1.remove(); + html.document.body.append(builder2.build().webOnlyRootElement); + + await matchGoldenFile('canvas_draw_picture_acrossframes.png', + region: region); + }); + + test('draw growing picture across frames clipped', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + builder.pushClipRect( + const Rect.fromLTRB(0, 0, 100, 100), + ); + + _drawTestPicture(builder, 100, true); + builder.pop(); + + html.Element elm1 = builder.build().webOnlyRootElement; + html.document.body.append(elm1); + + // Now draw picture again but at larger size. + final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder(); + builder2.pushClipRect( + const Rect.fromLTRB(0, 0, 100, 100), + ); + _drawTestPicture(builder2, 20, true); + builder2.pop(); + + elm1.remove(); + html.document.body.append(builder2.build().webOnlyRootElement); + + await matchGoldenFile('canvas_draw_picture_acrossframes_clipped.png', + region: region); + }); +} + +HtmlImage sharedImage; + +void _drawTestPicture(SceneBuilder builder, double targetSize, bool clipped) { + sharedImage ??= _createRealTestImage(); + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = + recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100)); + canvas.debugEnforceArbitraryPaint(); + if (clipped) { + canvas.clipRRect( + RRect.fromLTRBR(0, 0, targetSize, targetSize, Radius.circular(4))); + } + canvas.drawImageRect(sharedImage, Rect.fromLTWH(0, 0, 20, 20), + Rect.fromLTWH(0, 0, targetSize, targetSize), Paint()); + final Picture picture = recorder.endRecording(); + builder.addPicture( + Offset.zero, + picture, + ); +} + +typedef PaintCallback = void Function(RecordingCanvas canvas); + +const String _base64Encoded20x20TestImage = + 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA' + 'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj' + 'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg=='; + +HtmlImage _createRealTestImage() { + return HtmlImage( + html.ImageElement() + ..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage', + 20, + 20, + ); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index e53d37d3ab080..fd7f45e6a0023 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -222,12 +222,15 @@ void main() async { ); final SceneBuilder sb = SceneBuilder(); + sb.pushTransform(Matrix4.diagonal3Values(EngineWindow.browserDevicePixelRatio, + EngineWindow.browserDevicePixelRatio, 1.0).storage); sb.pushTransform(Matrix4.rotationZ(math.pi / 2).storage); sb.pushOffset(0, -500); sb.pushClipRect(canvasSize); sb.pop(); sb.pop(); sb.pop(); + sb.pop(); final SurfaceScene scene = sb.build(); final html.Element sceneElement = scene.webOnlyRootElement; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart new file mode 100644 index 0000000000000..e92f55287a252 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart @@ -0,0 +1,98 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart' hide TextStyle; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + const double screenWidth = 600.0; + const double screenHeight = 800.0; + const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); + final Paint testPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFFFF00FF); + + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/51514 + test('Canvas is reused and z-index doesn\'t leak across paints', () async { + final EngineCanvas engineCanvas = BitmapCanvas(screenRect); + const Rect region = Rect.fromLTWH(0, 0, 500, 500); + + // Draw first frame into engine canvas. + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400)); + final Path path = Path() + ..moveTo(3, 0) + ..lineTo(100, 97); + rc.drawPath(path, testPaint); + rc.apply(engineCanvas); + engineCanvas.endOfPaint(); + + html.Element sceneElement = html.Element.tag('flt-scene'); + sceneElement.append(engineCanvas.rootElement); + html.document.body.append(sceneElement); + + final html.CanvasElement canvas = html.document.querySelector('canvas'); + // ! Since canvas is first element, it should have zIndex = -1 for correct + // paint order. + expect(canvas.style.zIndex , '-1'); + + // Add id to canvas element to test for reuse. + const String kTestId = 'test-id-5698'; + canvas.id = kTestId; + + sceneElement.remove(); + // Clear so resources are marked for reuse. + + engineCanvas.clear(); + + // Now paint a second scene to same [BitmapCanvas] but paint an image + // before the path to move canvas element into second position. + final RecordingCanvas rc2 = + RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400)); + final Path path2 = Path() + ..moveTo(3, 0) + ..quadraticBezierTo(100, 0, 100, 100); + rc2.drawImage(_createRealTestImage(), Offset(0, 0), Paint()); + rc2.drawPath(path2, testPaint); + rc2.apply(engineCanvas); + + sceneElement = html.Element.tag('flt-scene'); + sceneElement.append(engineCanvas.rootElement); + html.document.body.append(sceneElement); + + final html.CanvasElement canvas2 = html.document.querySelector('canvas'); + // ZIndex should have been cleared since we have image element preceding + // canvas. + expect(canvas.style.zIndex != '-1', true); + expect(canvas2.id, kTestId); + await matchGoldenFile('bitmap_canvas_reuse_zindex.png', region: region); + }); +} + +const String _base64Encoded20x20TestImage = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA' + 'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj' + 'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg=='; + +HtmlImage _createRealTestImage() { + return HtmlImage( + html.ImageElement() + ..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage', + 20, + 20, + ); +} diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 455bceb07551f..076fff0ff3853 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -405,6 +405,11 @@ void _testCullRectComputation() { // enough to fit the rotated clip. test('clips correctly when using 3d transforms', () async { final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + + builder.pushTransform(Matrix4.diagonal3Values( + EngineWindow.browserDevicePixelRatio, + EngineWindow.browserDevicePixelRatio, 1.0).storage); + // TODO(yjbanov): see the TODO below. // final double screenWidth = html.window.innerWidth.toDouble(); // final double screenHeight = html.window.innerHeight.toDouble(); @@ -499,6 +504,7 @@ void _testCullRectComputation() { builder.pop(); // pushClipRect builder.pop(); // pushOffset builder.pop(); // pushTransform scale + builder.pop(); // pushTransform scale devicepixelratio html.document.body.append(builder.build().webOnlyRootElement); await matchGoldenFile('compositing_3d_rotate1.png', region: region); diff --git a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart index dcabb01fdbde1..38379087867e2 100644 --- a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart @@ -4,7 +4,6 @@ // @dart = 2.6 import 'dart:html' as html; -import 'dart:typed_data'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; @@ -63,8 +62,6 @@ void main() async { test('Should calculate tangent on cubic curve', () async { final Path path = Path(); - double p0x = 150; - double p0y = 20; double p1x = 240; double p1y = 120; double p2x = 320; @@ -112,8 +109,6 @@ void main() async { final Path path = Path(); path.moveTo(50, 130); path.lineTo(150, 20); - double p0x = 150; - double p0y = 20; double p1x = 240; double p1y = 120; double p2x = 320; @@ -122,8 +117,6 @@ void main() async { rc.drawPath(path, paint); - final Float32List buffer = Float32List(6); - List points = [p0x, p0y, p1x, p1y, p2x, p2y]; double t0 = 0.2; double t1 = 0.7; @@ -168,8 +161,6 @@ void main() async { final Path path = Path(); path.moveTo(50, 130); path.lineTo(150, 20); - double p0x = 150; - double p0y = 20; double p1x = 40; double p1y = 120; double p2x = 300; @@ -180,8 +171,6 @@ void main() async { rc.drawPath(path, paint); - final Float32List buffer = Float32List(6); - List points = [p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y]; double t0 = 0.2; double t1 = 0.7; diff --git a/lib/web_ui/test/golden_tests/engine/scuba.dart b/lib/web_ui/test/golden_tests/engine/scuba.dart index 421f4ae758be6..6ae5739577be5 100644 --- a/lib/web_ui/test/golden_tests/engine/scuba.dart +++ b/lib/web_ui/test/golden_tests/engine/scuba.dart @@ -71,7 +71,7 @@ class EngineScubaTester { sceneElement.append(canvas.rootElement); html.document.body.append(sceneElement); String screenshotName = '${fileName}_${canvas.runtimeType}'; - if (TextMeasurementService.enableExperimentalCanvasImplementation) { + if (WebExperiments.instance.useCanvasText) { screenshotName += '+canvas_measurement'; } await diffScreenshot( @@ -96,18 +96,20 @@ void testEachCanvas(String description, CanvasTest body, test('$description (bitmap)', () { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); + WebExperiments.instance.useCanvasText = false; return body(BitmapCanvas(bounds)); } finally { + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }); test('$description (bitmap + canvas measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; await body(BitmapCanvas(bounds)); } finally { - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }); diff --git a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart index d5f3dc8388e7c..35651a42276e8 100644 --- a/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/shadow_golden_test.dart @@ -12,7 +12,7 @@ import 'package:web_engine_tester/golden_tester.dart'; import 'scuba.dart'; -const Color _kShadowColor = Color.fromARGB(255, 255, 0, 0); +const Color _kShadowColor = Color.fromARGB(255, 0, 0, 0); void main() async { final Rect region = Rect.fromLTWH(0, 0, 550, 300); diff --git a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart index 167921f337f5b..507be4fd33df9 100644 --- a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart @@ -5,7 +5,7 @@ // @dart = 2.6 import 'dart:async'; -import 'package:ui/ui.dart'; +import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; import 'scuba.dart'; @@ -89,7 +89,7 @@ void main() async { offset = offset.translate(0, p.height + 10); // Only the first line is rendered with an ellipsis. - if (!TextMeasurementService.enableExperimentalCanvasImplementation) { + if (!WebExperiments.instance.useCanvasText) { // This is now correct with the canvas-based measurement, so we shouldn't // print the "(wrong)" warning. p = warning('(wrong)'); @@ -106,7 +106,7 @@ void main() async { // Only the first two lines are rendered and the ellipsis appears on the 2nd // line. - if (!TextMeasurementService.enableExperimentalCanvasImplementation) { + if (!WebExperiments.instance.useCanvasText) { // This is now correct with the canvas-based measurement, so we shouldn't // print the "(wrong)" warning. p = warning('(wrong)'); diff --git a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart index e46d8c258c277..c8ec261e8b72a 100644 --- a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. // @dart = 2.6 -import 'package:test/test.dart'; import 'package:ui/ui.dart'; import 'package:ui/src/engine.dart'; diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index 1b9e0eda0bf71..c365f3c69a6b1 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -242,7 +242,7 @@ void main() { Keyboard.instance.dispose(); }); - test('ignores keyboard events triggered on text fields', () { + test('keyboard events should be triggered on text fields', () { Keyboard.initialize(); int count = 0; @@ -260,7 +260,7 @@ void main() { ); expect(event.defaultPrevented, isFalse); - expect(count, 0); + expect(count, 1); }); Keyboard.instance.dispose(); diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 08ac0d778fbf2..6aa9d0015bef6 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -3,11 +3,17 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + WebExperiments.ensureInitialized(); + Profiler.ensureInitialized(); + }); + test('Should be able to build and layout a paragraph', () { final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); builder.addText('Hello'); diff --git a/lib/web_ui/test/paragraph_test.dart b/lib/web_ui/test/paragraph_test.dart index 29d52b830218f..c3432a00bed1f 100644 --- a/lib/web_ui/test/paragraph_test.dart +++ b/lib/web_ui/test/paragraph_test.dart @@ -4,7 +4,7 @@ // @dart = 2.6 import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart'; +import 'package:ui/ui.dart' hide window; import 'package:test/test.dart'; @@ -12,18 +12,20 @@ void testEachMeasurement(String description, VoidCallback body, {bool skip}) { test('$description (dom measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); + WebExperiments.instance.useCanvasText = false; return body(); } finally { + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }, skip: skip); test('$description (canvas measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; return body(); } finally { - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }, skip: skip); @@ -184,7 +186,7 @@ void main() async { test('getPositionForOffset multi-line', () { // [Paragraph.getPositionForOffset] for multi-line text doesn't work well // with dom-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -280,11 +282,11 @@ void main() async { ); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); test('getPositionForOffset multi-line centered', () { - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -387,7 +389,7 @@ void main() async { ); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('getBoxesForRange returns a box', () { @@ -782,7 +784,7 @@ void main() async { test('longestLine', () { // [Paragraph.longestLine] is only supported by canvas-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -797,7 +799,7 @@ void main() async { expect(paragraph.longestLine, 50.0); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('getLineBoundary (single-line)', () { @@ -824,7 +826,7 @@ void main() async { test('getLineBoundary (multi-line)', () { // [Paragraph.getLineBoundary] for multi-line paragraphs is only supported // by canvas-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -867,7 +869,7 @@ void main() async { } TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('width should be a whole integer', () { diff --git a/lib/web_ui/test/spy.dart b/lib/web_ui/test/spy.dart index e044a890eef84..ed7596e7313ad 100644 --- a/lib/web_ui/test/spy.dart +++ b/lib/web_ui/test/spy.dart @@ -23,7 +23,7 @@ class PlatformMessage { String get methodName => methodCall.method; /// Shorthand for getting the arguments of the method call. - String get methodArguments => methodCall.arguments; + dynamic get methodArguments => methodCall.arguments; } /// Intercepts platform messages sent from the engine to the framework. diff --git a/lib/web_ui/test/text/measurement_test.dart b/lib/web_ui/test/text/measurement_test.dart index 919e5fc97ae61..cf96ab0ef25b9 100644 --- a/lib/web_ui/test/text/measurement_test.dart +++ b/lib/web_ui/test/text/measurement_test.dart @@ -1154,5 +1154,6 @@ EngineLineMetrics line( width: width, lineNumber: lineNumber, left: left, + endIndexWithoutNewlines: -1, ); } diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index dad9bbdd80af7..6b64aaef49e77 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -7,12 +7,12 @@ import 'dart:html'; import 'dart:js_util' as js_util; import 'dart:typed_data'; -import 'package:ui/ui.dart' as ui; import 'package:ui/src/engine.dart' hide window; import 'package:test/test.dart'; import 'matchers.dart'; +import 'spy.dart'; /// The `keyCode` of the "Enter" key. const int _kReturnKeyCode = 13; @@ -560,7 +560,7 @@ void main() { /// Emulates sending of a message by the framework to the engine. void sendFrameworkMessage(dynamic message) { - textEditing.channel.handleTextInput(message); + textEditing.channel.handleTextInput(message, (ByteData data) {}); } /// Sends the necessary platform messages to activate a text field and show @@ -597,11 +597,11 @@ void main() { setUp(() { textEditing = HybridTextEditing(); - spy.activate(); + spy.setUp(); }); tearDown(() { - spy.deactivate(); + spy.tearDown(); if (textEditing.isEditing) { textEditing.stopEditing(); } @@ -699,15 +699,15 @@ void main() { textEditing.editingElement.domElement.blur(); expect(spy.messages, hasLength(1)); - MethodCall call = spy.messages[0]; - spy.messages.clear(); - expect(call.method, 'TextInputClient.onConnectionClosed'); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed'); expect( - call.arguments, + spy.messages[0].methodArguments, [ 123, // Client ID ], ); + spy.messages.clear(); // Input element is removed from DOM. expect(document.getElementsByTagName('input'), hasLength(0)); }, @@ -998,11 +998,10 @@ void main() { input.dispatchEvent(Event.eventType('Event', 'input')); expect(spy.messages, hasLength(1)); - MethodCall call = spy.messages[0]; - spy.messages.clear(); - expect(call.method, 'TextInputClient.updateEditingState'); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState'); expect( - call.arguments, + spy.messages[0].methodArguments, [ 123, // Client ID { @@ -1012,6 +1011,7 @@ void main() { } ], ); + spy.messages.clear(); input.setSelectionRange(2, 5); if (browserEngine == BrowserEngine.firefox) { @@ -1022,11 +1022,10 @@ void main() { } expect(spy.messages, hasLength(1)); - call = spy.messages[0]; - spy.messages.clear(); - expect(call.method, 'TextInputClient.updateEditingState'); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState'); expect( - call.arguments, + spy.messages[0].methodArguments, [ 123, // Client ID { @@ -1036,6 +1035,7 @@ void main() { } ], ); + spy.messages.clear(); hideKeyboard(); }); @@ -1078,20 +1078,35 @@ void main() { // Two messages should've been sent. One for the 'input' event and one for // the 'selectionchange' event. expect(spy.messages, hasLength(2)); - final MethodCall call = spy.messages.last; - spy.messages.clear(); - expect(call.method, 'TextInputClient.updateEditingState'); + + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState'); + expect( + spy.messages[0].methodArguments, + [ + 123, // Client ID + { + 'text': 'something\nelse', + 'selectionBase': 14, + 'selectionExtent': 14, + } + ], + ); + + expect(spy.messages[1].channel, 'flutter/textinput'); + expect(spy.messages[1].methodName, 'TextInputClient.updateEditingState'); expect( - call.arguments, + spy.messages[1].methodArguments, [ 123, // Client ID { 'text': 'something\nelse', 'selectionBase': 2, - 'selectionExtent': 5 + 'selectionExtent': 5, } ], ); + spy.messages.clear(); const MethodCall hide = MethodCall('TextInput.hide'); sendFrameworkMessage(codec.encodeMethodCall(hide)); @@ -1171,10 +1186,10 @@ void main() { ); expect(spy.messages, hasLength(1)); - final MethodCall call = spy.messages.first; - expect(call.method, 'TextInputClient.performAction'); + expect(spy.messages[0].channel, 'flutter/textinput'); + expect(spy.messages[0].methodName, 'TextInputClient.performAction'); expect( - call.arguments, + spy.messages[0].methodArguments, [clientId, 'TextInputAction.next'], ); }, @@ -1384,30 +1399,6 @@ void checkTextAreaEditingState( expect(textarea.selectionEnd, end); } -class PlatformMessagesSpy { - bool _isActive = false; - ui.PlatformMessageCallback _backup; - - final List messages = []; - - void activate() { - assert(!_isActive); - _isActive = true; - _backup = ui.window.onPlatformMessage; - ui.window.onPlatformMessage = (String channel, ByteData data, - ui.PlatformMessageResponseCallback callback) { - messages.add(codec.decodeMethodCall(data)); - }; - } - - void deactivate() { - assert(_isActive); - _isActive = false; - messages.clear(); - ui.window.onPlatformMessage = _backup; - } -} - Map createFlutterConfig( String inputType, { bool obscureText = false, diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index a20bffe3877df..4062be970c3f1 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -3,9 +3,15 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:typed_data'; + import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; +const MethodCodec codec = JSONMethodCodec(); + +void emptyCallback(ByteData date) {} + TestLocationStrategy _strategy; TestLocationStrategy get strategy => _strategy; set strategy(TestLocationStrategy newStrategy) { @@ -17,7 +23,27 @@ void main() { strategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); expect(window.defaultRouteName, '/initial'); + // Changing the URL in the address bar later shouldn't affect [window.defaultRouteName]. strategy.replaceState(null, null, '/newpath'); expect(window.defaultRouteName, '/initial'); }); + + test('window.defaultRouteName should reset after navigation platform message', () { + strategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); + // Reading it multiple times should return the same value. + expect(window.defaultRouteName, '/initial'); + expect(window.defaultRouteName, '/initial'); + + window.sendPlatformMessage( + 'flutter/navigation', + JSONMethodCodec().encodeMethodCall(MethodCall( + 'routePushed', + {'previousRouteName': '/foo', 'routeName': '/bar'}, + )), + emptyCallback, + ); + // After a navigation platform message, [window.defaultRouteName] should + // reset to "/". + expect(window.defaultRouteName, '/'); + }); } diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 055d049c2640b..3e78de7473e38 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -137,7 +137,8 @@ DartIsolate::DartIsolate(const Settings& settings, settings.log_tag, settings.unhandled_exception_callback, DartVMRef::GetIsolateNameServer()), - is_root_isolate_(is_root_isolate) { + is_root_isolate_(is_root_isolate), + disable_http_(settings.disable_http) { phase_ = Phase::Uninitialized; } @@ -222,9 +223,9 @@ bool DartIsolate::UpdateThreadPoolNames() const { // shells sharing the same (or subset of) threads. const auto& task_runners = GetTaskRunners(); - if (auto task_runner = task_runners.GetGPUTaskRunner()) { + if (auto task_runner = task_runners.GetRasterTaskRunner()) { task_runner->PostTask( - [label = task_runners.GetLabel() + std::string{".gpu"}]() { + [label = task_runners.GetLabel() + std::string{".raster"}]() { Dart_SetThreadName(label.c_str()); }); } @@ -261,7 +262,7 @@ bool DartIsolate::LoadLibraries() { tonic::DartState::Scope scope(this); - DartIO::InitForIsolate(); + DartIO::InitForIsolate(disable_http_); DartUI::InitForIsolate(IsRootIsolate()); @@ -687,7 +688,7 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( parent_group_data.GetIsolateShutdownCallback()))); TaskRunners null_task_runners(advisory_script_uri, - /* platform= */ nullptr, /* gpu= */ nullptr, + /* platform= */ nullptr, /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); @@ -729,7 +730,7 @@ bool DartIsolate::DartIsolateInitializeCallback(void** child_callback_data, Dart_CurrentIsolateGroupData()); TaskRunners null_task_runners((*isolate_group_data)->GetAdvisoryScriptURI(), - /* platform= */ nullptr, /* gpu= */ nullptr, + /* platform= */ nullptr, /* raster= */ nullptr, /* ui= */ nullptr, /* io= */ nullptr); diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index 22f6ea5b98bd0..4a045d38d422c 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -402,6 +402,7 @@ class DartIsolate : public UIDartState { std::vector> shutdown_callbacks_; fml::RefPtr message_handling_task_runner_; const bool is_root_isolate_; + const bool disable_http_; DartIsolate(const Settings& settings, TaskRunners task_runners, diff --git a/runtime/dart_service_isolate.cc b/runtime/dart_service_isolate.cc index 218c3bb6035d9..5632738a10891 100644 --- a/runtime/dart_service_isolate.cc +++ b/runtime/dart_service_isolate.cc @@ -76,7 +76,7 @@ void DartServiceIsolate::NotifyServerState(Dart_NativeArguments args) { } } - for (auto callback_to_fire : callbacks_to_fire) { + for (const auto& callback_to_fire : callbacks_to_fire) { callback_to_fire(uri); } } diff --git a/runtime/dart_service_isolate.h b/runtime/dart_service_isolate.h index 5bd9b5beb3f65..1649d3e3343ed 100644 --- a/runtime/dart_service_isolate.h +++ b/runtime/dart_service_isolate.h @@ -11,16 +11,54 @@ #include #include "flutter/fml/compiler_specific.h" - #include "third_party/dart/runtime/include/dart_api.h" namespace flutter { +//------------------------------------------------------------------------------ +/// @brief Utility methods for interacting with the DartVM managed service +/// isolate present in debug and profile runtime modes. +/// class DartServiceIsolate { public: + //---------------------------------------------------------------------------- + /// The handle used to refer to callbacks registered with the service isolate. + /// + using CallbackHandle = ptrdiff_t; + + //---------------------------------------------------------------------------- + /// A callback made by the Dart VM when the observatory is ready. The argument + /// indicates the observatory URI. + /// using ObservatoryServerStateCallback = - std::function; + std::function; + //---------------------------------------------------------------------------- + /// @brief Start the service isolate. This call may only be made in the + /// Dart VM initiated isolate creation callback. It is only valid + /// to make this call when the VM explicitly requests the creation + /// of the service isolate. The VM does this by specifying the + /// script URI to be `DART_VM_SERVICE_ISOLATE_NAME`. The isolate + /// to be designated as the service isolate must already be + /// created (but not running) when this call is made. + /// + /// @param[in] server_ip The service protocol IP address. + /// @param[in] server_port The service protocol port. + /// @param[in] embedder_tag_handler The library tag handler. + /// @param[in] disable_origin_check If websocket origin checks must + /// be enabled. + /// @param[in] disable_service_auth_codes If service auth codes must be + /// enabled. + /// @param[in] enable_service_port_fallback If fallback to port 0 must be + /// enabled when the bind fails. + /// @param error The error when this method + /// returns false. This string must + /// be freed by the caller using + /// `free`. + /// + /// @return If the startup was successful. Refer to the `error` for + /// details on failure. + /// static bool Startup(std::string server_ip, intptr_t server_port, Dart_LibraryTagHandler embedder_tag_handler, @@ -29,14 +67,32 @@ class DartServiceIsolate { bool enable_service_port_fallback, char** error); - using CallbackHandle = ptrdiff_t; - - // Returns a handle for the callback that can be used in - // RemoveServerStatusCallback + //---------------------------------------------------------------------------- + /// @brief Add a callback that will get invoked when the observatory + /// starts up. If the observatory has already started before this + /// call is made, the callback is invoked immediately. + /// + /// This method is thread safe. + /// + /// @param[in] callback The callback with information about the observatory. + /// + /// @return A handle for the callback that can be used later in + /// `RemoveServerStatusCallback`. + /// [[nodiscard]] static CallbackHandle AddServerStatusCallback( const ObservatoryServerStateCallback& callback); - // Accepts the handle returned by AddServerStatusCallback + //---------------------------------------------------------------------------- + /// @brief Removed a callback previously registered via + /// `AddServiceStatusCallback`. + /// + /// This method is thread safe. + /// + /// @param[in] handle The handle + /// + /// @return If the callback was unregistered. This may fail if there was + /// no such callback with that handle. + /// static bool RemoveServerStatusCallback(CallbackHandle handle); private: diff --git a/runtime/dart_vm_data.h b/runtime/dart_vm_data.h index 0f054bf55f3d6..49ddd316899a4 100644 --- a/runtime/dart_vm_data.h +++ b/runtime/dart_vm_data.h @@ -10,19 +10,65 @@ namespace flutter { +//------------------------------------------------------------------------------ +/// @brief Provides thread-safe access to data that is necessary to +/// bootstrap a new Dart VM instance. All snapshots referenced by +/// this object are read-only. +/// class DartVMData { public: + //---------------------------------------------------------------------------- + /// @brief Creates a new instance of `DartVMData`. Both the VM and + /// isolate snapshot members are optional and may be `nullptr`. If + /// `nullptr`, the snapshot resolvers present in the settings + /// object are used to infer the snapshots. If the snapshots + /// cannot be inferred from the settings object, this method + /// return `nullptr`. + /// + /// @param[in] settings The settings used to infer the VM and + /// isolate snapshots if they are not provided + /// directly. + /// @param[in] vm_snapshot The VM snapshot or `nullptr`. + /// @param[in] isolate_snapshot The isolate snapshot or `nullptr`. + /// + /// @return A new instance of VM data that can be used to bootstrap a Dart + /// VM. `nullptr` if the snapshots are not provided and cannot be + /// inferred from the settings object. + /// static std::shared_ptr Create( Settings settings, fml::RefPtr vm_snapshot, fml::RefPtr isolate_snapshot); + //---------------------------------------------------------------------------- + /// @brief Collect the DartVMData instance. + /// ~DartVMData(); + //---------------------------------------------------------------------------- + /// @brief The settings object from which the Dart snapshots were + /// inferred. + /// + /// @return The settings. + /// const Settings& GetSettings() const; + //---------------------------------------------------------------------------- + /// @brief Gets the VM snapshot. This can be in the call to bootstrap + /// the Dart VM via `Dart_Initialize`. + /// + /// @return The VM snapshot. + /// const DartSnapshot& GetVMSnapshot() const; + //---------------------------------------------------------------------------- + /// @brief Get the isolate snapshot necessary to launch isolates in the + /// Dart VM. The Dart VM instance in which these isolates are + /// launched must be the same as the VM created using snapshot + /// accessed via `GetVMSnapshot`. + /// + /// @return The isolate snapshot. + /// fml::RefPtr GetIsolateSnapshot() const; private: diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index cea325bad7444..33b51fe0690b7 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -22,13 +22,89 @@ #include "rapidjson/stringbuffer.h" namespace flutter { + class Scene; class RuntimeDelegate; class View; class Window; +//------------------------------------------------------------------------------ +/// Represents an instance of a running root isolate with window bindings. In +/// normal operation, a single instance of this object is owned by the engine +/// per shell. This object may only be created, used, and collected on the UI +/// task runner. Window state queried by the root isolate is stored by this +/// object. In cold-restart scenarios, the engine may collect this before +/// installing a new runtime controller in its place. The Clone method may be +/// used by the engine to copy the currently accumulated window state so it can +/// be referenced by the new runtime controller. +/// class RuntimeController final : public WindowClient { public: + //---------------------------------------------------------------------------- + /// @brief Creates a new instance of a runtime controller. This is + /// usually only done by the engine instance associated with the + /// shell. + /// + /// @param client The runtime delegate. This is + /// usually the `Engine` instance. + /// @param vm A reference to a running Dart VM. + /// The runtime controller must be + /// collected before the VM is + /// destroyed (this order is + /// guaranteed by the shell). + /// @param[in] isolate_snapshot The isolate snapshot used to start + /// the root isolate managed by this + /// runtime controller. The isolate + /// must be transitioned into the + /// running phase manually by the + /// caller. + /// @param[in] task_runners The task runners used by the shell + /// hosting this runtime controller. + /// This may be used by the isolate to + /// scheduled asynchronous texture + /// uploads or post tasks to the + /// platform task runner. + /// @param[in] snapshot_delegate The snapshot delegate used by the + /// isolate to gather raster snapshots + /// of Flutter view hierarchies. + /// @param[in] io_manager The IO manager used by the isolate + /// for asynchronous texture uploads. + /// @param[in] unref_queue The unref queue used by the + /// isolate to collect resources that + /// may reference resources on the + /// GPU. + /// @param[in] image_decoder The image decoder + /// @param[in] advisory_script_uri The advisory script URI (only used + /// for debugging). This does not + /// affect the code being run in the + /// isolate in any way. + /// @param[in] advisory_script_entrypoint The advisory script entrypoint + /// (only used for debugging). This + /// does not affect the code being run + /// in the isolate in any way. The + /// isolate must be transitioned to + /// the running state explicitly by + /// the caller. + /// @param[in] idle_notification_callback The idle notification callback. + /// This allows callers to run native + /// code in isolate scope when the VM + /// is about to be notified that the + /// engine is going to be idle. + /// @param[in] window_data The window data (if exists). + /// @param[in] isolate_create_callback The isolate create callback. This + /// allows callers to run native code + /// in isolate scope on the UI task + /// runner as soon as the root isolate + /// has been created. + /// @param[in] isolate_shutdown_callback The isolate shutdown callback. + /// This allows callers to run native + /// code in isolate scoped on the UI + /// task runner just as the root + /// isolate is about to be torn down. + /// @param[in] persistent_isolate_data Unstructured persistent read-only + /// data that the root isolate can + /// access in a synchronous manner. + /// RuntimeController( RuntimeDelegate& client, DartVM* vm, @@ -41,53 +117,326 @@ class RuntimeController final : public WindowClient { std::string advisory_script_uri, std::string advisory_script_entrypoint, const std::function& idle_notification_callback, - const WindowData& data, + const WindowData& window_data, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, std::shared_ptr persistent_isolate_data); + // |WindowClient| ~RuntimeController() override; + //---------------------------------------------------------------------------- + /// @brief Clone the the runtime controller. This re-creates the root + /// isolate with the same snapshots and copies all window data to + /// the new instance. This is usually only used in the debug + /// runtime mode to support the cold-restart scenario. + /// + /// @return A clone of the existing runtime controller. + /// std::unique_ptr Clone() const; + //---------------------------------------------------------------------------- + /// @brief Forward the specified window metrics to the running isolate. + /// If the isolate is not running, these metrics will be saved and + /// flushed to the isolate when it starts. + /// + /// @param[in] metrics The metrics. + /// + /// @return If the window metrics were forwarded to the running isolate. + /// bool SetViewportMetrics(const ViewportMetrics& metrics); + //---------------------------------------------------------------------------- + /// @brief Forward the specified locale data to the running isolate. If + /// the isolate is not running, this data will be saved and + /// flushed to the isolate when it starts running. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] locale_data The locale data + /// + /// @return If the locale data was forwarded to the running isolate. + /// bool SetLocales(const std::vector& locale_data); + //---------------------------------------------------------------------------- + /// @brief Forward the user settings data to the running isolate. If the + /// isolate is not running, this data will be saved and flushed to + /// the isolate when it starts running. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The user settings data. + /// + /// @return If the user settings data was forwarded to the running + /// isolate. + /// bool SetUserSettingsData(const std::string& data); + //---------------------------------------------------------------------------- + /// @brief Forward the lifecycle state data to the running isolate. If + /// the isolate is not running, this data will be saved and + /// flushed to the isolate when it starts running. + /// + /// @deprecated The persistent isolate data must be used for this purpose + /// instead. + /// + /// @param[in] data The lifecycle state data. + /// + /// @return If the lifecycle state data was forwarded to the running + /// isolate. + /// bool SetLifecycleState(const std::string& data); + //---------------------------------------------------------------------------- + /// @brief Notifies the running isolate about whether the semantics tree + /// should be generated or not. If the isolate is not running, + /// this preference will be saved and flushed to the isolate when + /// it starts running. + /// + /// @param[in] enabled Indicates whether to generate the semantics tree. + /// + /// @return If the semantics tree generation preference was forwarded to + /// the running isolate. + /// bool SetSemanticsEnabled(bool enabled); + //---------------------------------------------------------------------------- + /// @brief Forward the preference of accessibility features that must be + /// enabled in the semantics tree to the running isolate. If the + /// isolate is not running, this data will be saved and flushed to + /// the isolate when it starts running. + /// + /// @param[in] flags The accessibility features that must be generated in + /// the semantics tree. + /// + /// @return If the preference of accessibility features was forwarded to + /// the running isolate. + /// bool SetAccessibilityFeatures(int32_t flags); + //---------------------------------------------------------------------------- + /// @brief Notifies the running isolate that it should start generating a + /// new frame. + /// + /// @see `Engine::BeginFrame` for more context. + /// + /// @param[in] frame_time The point at which the current frame interval + /// began. May be used by animation interpolators, + /// physics simulations, etc. + /// + /// @return If notification to begin frame rendering was delivered to the + /// running isolate. + /// bool BeginFrame(fml::TimePoint frame_time); + //---------------------------------------------------------------------------- + /// @brief Dart code cannot fully measure the time it takes for a + /// specific frame to be rendered. This is because Dart code only + /// runs on the UI task runner. That is only a small part of the + /// overall frame workload. The GPU task runner frame workload is + /// executed on a thread where Dart code cannot run (and hence + /// instrument). Besides, due to the pipelined nature of rendering + /// in Flutter, there may be multiple frame workloads being + /// processed at any given time. However, for non-Timeline based + /// profiling, it is useful for trace collection and processing to + /// happen in Dart. To do this, the GPU task runner frame + /// workloads need to be instrumented separately. After a set + /// number of these profiles have been gathered, they need to be + /// reported back to Dart code. The engine reports this extra + /// instrumentation information back to Dart code running on the + /// engine by invoking this method at predefined intervals. + /// + /// @see `Engine::ReportTimings`, `FrameTiming` + /// + /// @param[in] timings Collection of `FrameTiming::kCount` * `n` timestamps + /// for `n` frames whose timings have not been reported + /// yet. A collection of integers is reported here for + /// easier conversions to Dart objects. The timestamps + /// are measured against the system monotonic clock + /// measured in microseconds. + /// bool ReportTimings(std::vector timings); + //---------------------------------------------------------------------------- + /// @brief Notify the Dart VM that no frame workloads are expected on the + /// UI task runner till the specified deadline. The VM uses this + /// opportunity to perform garbage collection operations is a + /// manner that interferes as little as possible with frame + /// rendering. + /// + /// NotifyIdle is advisory. The VM may or may not run a garbage collection + /// when this is called, and will eventually perform garbage collections even + /// if it is not called or it is called with insufficient deadlines. + /// + /// The garbage collection mechanism and its thresholds are internal + /// implementation details and absolutely no guarantees are made about the + /// threshold discussed below. This discussion is also an oversimplification + /// but hopefully serves to calibrate expectations about GC behavior: + /// * When the Dart VM and its root isolate are initialized, the memory + /// consumed upto that point are treated as a baseline. + /// * A fixed percentage of the memory consumed (~20%) over the baseline is + /// treated as the hard threshold. + /// * The memory in play is divided into old space and new space. The new + /// space is typically very small and fills up rapidly. + /// * The baseline plus the threshold is considered the old space while the + /// small new space is a separate region (typically a few pages). + /// * The total old space size minus the max new space size is treated as the + /// soft threshold. + /// * In a world where there is no call to NotifyIdle, when the total + /// allocation exceeds the soft threshold, a concurrent mark is initiated in + /// the VM. There is a “small” pause that occurs when the concurrent mark is + /// initiated and another pause when the mark concludes and a sweep is + /// initiated. + /// * If the total allocations exceeds the the hard threshold, a “big” + /// stop-the-world pause is initiated. + /// * If after either the sweep after the concurrent mark, or, the + /// stop-the-world pause, the consumption returns to be below the soft + /// threshold, the dance begins anew. + /// * If after both the “small” and “big” pauses, memory usage is still over + /// the hard threshold, i.e, the objects are still reachable, that amount of + /// memory is treated as the new baseline and a fixed percentage of the new + /// baseline over the new baseline is now the new hard threshold. + /// * Updating the baseline will continue till memory for the updated old + /// space can be allocated from the operating system. These allocations will + /// typically fail due to address space exhaustion on 32-bit systems and + /// page table exhaustion on 64-bit systems. + /// * NotifyIdle initiates the concurrent mark preemptively. The deadline is + /// used by the VM to determine if the corresponding sweep can be performed + /// within the deadline. This way, jank due to “small” pauses can be + /// ameliorated. + /// * There is no ability to stop a “big” pause on reaching the hard threshold + /// in the old space. The best you can do is release (by making them + /// unreachable) objects eagerly so that the are marked as unreachable in + /// the concurrent mark initiated by either reaching the soft threshold or + /// an explicit NotifyIdle. + /// * If you are running out of memory, its because too many large objects + /// were allocation and remained reachable such that the the old space kept + /// growing till it could grow no more. + /// * At the edges of allocation thresholds, failures can occur gracefully if + /// the instigating allocation was made in the Dart VM or rather gracelessly + /// if the allocation is made by some native component. + /// + /// @see `Dart_TimelineGetMicros` + /// + /// @bug The `deadline` argument must be converted to `std::chrono` + /// instead of a raw integer. + /// + /// @param[in] deadline The deadline measures in microseconds against the + /// system's monotonic time. The clock can be accessed via + /// `Dart_TimelineGetMicros`. + /// + /// @return If the idle notification was forwarded to the running isolate. + /// bool NotifyIdle(int64_t deadline); + //---------------------------------------------------------------------------- + /// @brief Returns if the root isolate is running. The isolate must be + /// transitioned to the running phase manually. The isolate can + /// stop running if it terminates execution on its own. + /// + /// @return True if root isolate running, False otherwise. + /// bool IsRootIsolateRunning() const; + //---------------------------------------------------------------------------- + /// @brief Dispatch the specified platform message to running root + /// isolate. + /// + /// @param[in] message The message to dispatch to the isolate. + /// + /// @return If the message was dispatched to the running root isolate. + /// This may fail is an isolate is not running. + /// bool DispatchPlatformMessage(fml::RefPtr message); + //---------------------------------------------------------------------------- + /// @brief Dispatch the specified pointer data message to the running + /// root isolate. + /// + /// @param[in] packet The pointer data message to dispatch to the isolate. + /// + /// @return If the pointer data message was dispatched. This may fail is + /// an isolate is not running. + /// bool DispatchPointerDataPacket(const PointerDataPacket& packet); + //---------------------------------------------------------------------------- + /// @brief Dispatch the semantics action to the specified accessibility + /// node. + /// + /// @param[in] id The identified of the accessibility node. + /// @param[in] action The semantics action to perform on the specified + /// accessibility node. + /// @param[in] args Optional data that applies to the specified action. + /// + /// @return If the semantics action was dispatched. This may fail if an + /// isolate is not running. + /// bool DispatchSemanticsAction(int32_t id, SemanticsAction action, std::vector args); + //---------------------------------------------------------------------------- + /// @brief Gets the main port identifier of the root isolate. + /// + /// @return The main port identifier. If no root isolate is running, + /// returns `ILLEGAL_PORT`. + /// Dart_Port GetMainPort(); + //---------------------------------------------------------------------------- + /// @brief Gets the debug name of the root isolate. But default, the + /// debug name of the isolate is derived from its advisory script + /// URI, advisory main entrypoint and its main port name. For + /// example, "main.dart$main-1234" where the script URI is + /// "main.dart", the entrypoint is "main" and the port name + /// "1234". Once launched, the isolate may re-christen itself + /// using a name it selects via `setIsolateDebugName` in + /// `window.dart`. This name is purely advisory and only used by + /// instrumentation and reporting purposes. + /// + /// @return The debug name of the root isolate. + /// std::string GetIsolateName(); + //---------------------------------------------------------------------------- + /// @brief Returns if the root isolate has any live receive ports. + /// + /// @return True if there are live receive ports, False otherwise. Return + /// False if the root isolate is not running as well. + /// bool HasLivePorts(); + //---------------------------------------------------------------------------- + /// @brief Get the last error encountered by the microtask queue. + /// + /// @return The last error encountered by the microtask queue. + /// tonic::DartErrorHandleType GetLastError(); + //---------------------------------------------------------------------------- + /// @brief Get a weak pointer to the root Dart isolate. This isolate may + /// only be locked on the UI task runner. Callers use this + /// accessor to transition to the root isolate to the running + /// phase. + /// + /// @return The root isolate reference. + /// std::weak_ptr GetRootIsolate(); + //---------------------------------------------------------------------------- + /// @brief Get the return code specified by the root isolate (if one is + /// present). + /// + /// @bug Change this method to return `std::optional` + /// instead. + /// + /// @return The root isolate return code. The first argument in the pair + /// indicates if one is specified by the root isolate. + /// std::pair GetRootIsolateReturnCode(); private: diff --git a/runtime/service_protocol.cc b/runtime/service_protocol.cc index 16d574de8f8c3..5c7ea0279a4a7 100644 --- a/runtime/service_protocol.cc +++ b/runtime/service_protocol.cc @@ -33,6 +33,8 @@ const std::string_view ServiceProtocol::kSetAssetBundlePathExtensionName = "_flutter.setAssetBundlePath"; const std::string_view ServiceProtocol::kGetDisplayRefreshRateExtensionName = "_flutter.getDisplayRefreshRate"; +const std::string_view ServiceProtocol::kGetSkSLsExtensionName = + "_flutter.getSkSLs"; static constexpr std::string_view kViewIdPrefx = "_flutterView/"; static constexpr std::string_view kListViewsExtensionName = @@ -50,6 +52,7 @@ ServiceProtocol::ServiceProtocol() kFlushUIThreadTasksExtensionName, kSetAssetBundlePathExtensionName, kGetDisplayRefreshRateExtensionName, + kGetSkSLsExtensionName, }), handlers_mutex_(fml::SharedMutex::Create()) {} diff --git a/runtime/service_protocol.h b/runtime/service_protocol.h index ce2a6845c24b4..c66a836a2c52e 100644 --- a/runtime/service_protocol.h +++ b/runtime/service_protocol.h @@ -27,6 +27,7 @@ class ServiceProtocol { static const std::string_view kFlushUIThreadTasksExtensionName; static const std::string_view kSetAssetBundlePathExtensionName; static const std::string_view kGetDisplayRefreshRateExtensionName; + static const std::string_view kGetSkSLsExtensionName; class Handler { public: diff --git a/runtime/skia_concurrent_executor.h b/runtime/skia_concurrent_executor.h index e1a506c033ab6..87dde23936325 100644 --- a/runtime/skia_concurrent_executor.h +++ b/runtime/skia_concurrent_executor.h @@ -11,13 +11,38 @@ namespace flutter { +//------------------------------------------------------------------------------ +/// @brief An interface used by Skia to schedule work on engine managed +/// threads (usually workers in a concurrent message loop). +/// +/// Skia may decide that certain workloads don't have thread +/// affinity and may be performed on a background thread. However, +/// Skia does not manage its own threads. So, it delegates the +/// scheduling of this work to the engine via this interface. The +/// engine has a dedicated pool of threads it uses for scheduling +/// background tasks that have no thread affinity. This thread +/// worker pool is held next to the process global Dart VM instance. +/// The Skia executor is wired up there as well. +/// class SkiaConcurrentExecutor : public SkExecutor { public: + //---------------------------------------------------------------------------- + /// The callback invoked by the executor to schedule the given task onto an + /// engine managed background thread. + /// using OnWorkCallback = std::function; + + //---------------------------------------------------------------------------- + /// @brief Create a new instance of the executor. + /// + /// @param[in] on_work The work callback. + /// SkiaConcurrentExecutor(const OnWorkCallback& on_work); + // |SkExecutor| ~SkiaConcurrentExecutor() override; + // |SkExecutor| void add(fml::closure work) override; private: diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index 8e4a48e2bb000..3fab3317b094b 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -12,11 +12,8 @@ if (is_fuchsia) { } config("vulkan_config") { - if (using_fuchsia_sdk) { + if (is_fuchsia) { include_dirs = [ "$fuchsia_sdk_root/vulkan/include" ] - } else if (is_fuchsia) { - include_dirs = - [ "//third_party/vulkan_loader_and_validation_layers/include" ] } else { include_dirs = [ "//third_party/vulkan/src" ] } @@ -194,6 +191,8 @@ if (enable_unittests) { "pipeline_unittests.cc", "shell_test.cc", "shell_test.h", + "shell_test_external_view_embedder.cc", + "shell_test_external_view_embedder.h", "shell_test_platform_view.cc", "shell_test_platform_view.h", "shell_unittests.cc", diff --git a/shell/common/animator.cc b/shell/common/animator.cc index 5ae3862a93ad1..322411a11b269 100644 --- a/shell/common/animator.cc +++ b/shell/common/animator.cc @@ -25,7 +25,8 @@ Animator::Animator(Delegate& delegate, : delegate_(delegate), task_runners_(std::move(task_runners)), waiter_(std::move(waiter)), - last_begin_frame_time_(), + last_frame_begin_time_(), + last_frame_target_time_(), dart_frame_deadline_(0), #if FLUTTER_SHELL_ENABLE_METAL layer_tree_pipeline_(fml::MakeRefCounted(2)), @@ -35,7 +36,7 @@ Animator::Animator(Delegate& delegate, // https://github.com/flutter/engine/pull/9132 for discussion. layer_tree_pipeline_(fml::MakeRefCounted( task_runners.GetPlatformTaskRunner() == - task_runners.GetGPUTaskRunner() + task_runners.GetRasterTaskRunner() ? 1 : 2)), #endif // FLUTTER_SHELL_ENABLE_METAL @@ -132,7 +133,8 @@ void Animator::BeginFrame(fml::TimePoint frame_start_time, // to service potential frame. FML_DCHECK(producer_continuation_); - last_begin_frame_time_ = frame_start_time; + last_frame_begin_time_ = frame_start_time; + last_frame_target_time_ = frame_target_time; dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time); { TRACE_EVENT2("flutter", "Framework Workload", "mode", "basic", "frame", @@ -178,7 +180,8 @@ void Animator::Render(std::unique_ptr layer_tree) { if (layer_tree) { // Note the frame time for instrumentation. - layer_tree->RecordBuildTime(last_begin_frame_time_); + layer_tree->RecordBuildTime(last_frame_begin_time_, + last_frame_target_time_); } // Commit the pending continuation. diff --git a/shell/common/animator.h b/shell/common/animator.h index 0bb15bed72a54..f96acb7e9a3a0 100644 --- a/shell/common/animator.h +++ b/shell/common/animator.h @@ -30,7 +30,7 @@ class Animator final { public: class Delegate { public: - virtual void OnAnimatorBeginFrame(fml::TimePoint frame_time) = 0; + virtual void OnAnimatorBeginFrame(fml::TimePoint frame_target_time) = 0; virtual void OnAnimatorNotifyIdle(int64_t deadline) = 0; @@ -96,7 +96,8 @@ class Animator final { TaskRunners task_runners_; std::shared_ptr waiter_; - fml::TimePoint last_begin_frame_time_; + fml::TimePoint last_frame_begin_time_; + fml::TimePoint last_frame_target_time_; int64_t dart_frame_deadline_; fml::RefPtr layer_tree_pipeline_; fml::Semaphore pending_frame_semaphore_; diff --git a/shell/common/animator_unittests.cc b/shell/common/animator_unittests.cc index 3907cee3b89ab..bf2ac9f5a9cf1 100644 --- a/shell/common/animator_unittests.cc +++ b/shell/common/animator_unittests.cc @@ -55,7 +55,7 @@ TEST_F(ShellTest, VSyncTargetTime) { return ShellTestPlatformView::Create( shell, shell.GetTaskRunners(), vsync_clock, std::move(create_vsync_waiter), - ShellTestPlatformView::BackendType::kDefaultBackend); + ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, [](Shell& shell) { return std::make_unique(shell, shell.GetTaskRunners()); @@ -84,10 +84,14 @@ TEST_F(ShellTest, VSyncTargetTime) { }); on_target_time_latch.Wait(); - ASSERT_EQ(ConstantFiringVsyncWaiter::frame_target_time.ToEpochDelta() - .ToMicroseconds(), + const auto vsync_waiter_target_time = + ConstantFiringVsyncWaiter::frame_target_time; + ASSERT_EQ(vsync_waiter_target_time.ToEpochDelta().ToMicroseconds(), target_time); + // validate that the latest target time has also been updated. + ASSERT_EQ(GetLatestFrameTargetTime(shell.get()), vsync_waiter_target_time); + // teardown. DestroyShell(std::move(shell), std::move(task_runners)); ASSERT_FALSE(DartVMRef::IsInstanceRunning()); diff --git a/shell/common/canvas_spy.cc b/shell/common/canvas_spy.cc index d31a86f18d158..5b62a34953a23 100644 --- a/shell/common/canvas_spy.cc +++ b/shell/common/canvas_spy.cc @@ -124,21 +124,6 @@ void DidDrawCanvas::onDrawPath(const SkPath& path, const SkPaint& paint) { MarkDrawIfNonTransparentPaint(paint); } -void DidDrawCanvas::onDrawBitmap(const SkBitmap& bitmap, - SkScalar x, - SkScalar y, - const SkPaint* paint) { - did_draw_ = true; -} - -void DidDrawCanvas::onDrawBitmapRect(const SkBitmap& bitmap, - const SkRect* src, - const SkRect& dst, - const SkPaint* paint, - SrcRectConstraint constraint) { - did_draw_ = true; -} - void DidDrawCanvas::onDrawImage(const SkImage* image, SkScalar left, SkScalar top, diff --git a/shell/common/canvas_spy.h b/shell/common/canvas_spy.h index 0fc466bb05938..d5c75be34df40 100644 --- a/shell/common/canvas_spy.h +++ b/shell/common/canvas_spy.h @@ -127,19 +127,6 @@ class DidDrawCanvas final : public SkCanvasVirtualEnforcer { // |SkCanvasVirtualEnforcer| void onDrawPath(const SkPath&, const SkPaint&) override; - // |SkCanvasVirtualEnforcer| - void onDrawBitmap(const SkBitmap&, - SkScalar left, - SkScalar top, - const SkPaint*) override; - - // |SkCanvasVirtualEnforcer| - void onDrawBitmapRect(const SkBitmap&, - const SkRect* src, - const SkRect& dst, - const SkPaint*, - SrcRectConstraint) override; - // |SkCanvasVirtualEnforcer| void onDrawImage(const SkImage*, SkScalar left, @@ -166,13 +153,6 @@ class DidDrawCanvas final : public SkCanvasVirtualEnforcer { const SkPaint*) override; // |SkCanvasVirtualEnforcer| -#ifdef SK_SUPPORT_LEGACY_DRAWVERTS_VIRTUAL - void onDrawVerticesObject(const SkVertices*, - const SkVertices::Bone[], - int, - SkBlendMode, - const SkPaint&) override {} -#endif void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; diff --git a/shell/common/engine.h b/shell/common/engine.h index b44e2fae78525..0f5f0717220b1 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -210,7 +210,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// is not possible for the application to determine the total /// time it took to render a specific frame. While the /// layer-tree is constructed on the UI thread, it needs to be - /// rendering on the GPU thread. Dart code cannot execute on + /// rendering on the raster thread. Dart code cannot execute on /// this thread. So any instrumentation about the frame times /// gathered on this thread needs to be aggregated and sent back /// to the UI thread for processing in Dart. @@ -412,7 +412,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// by layer tree in the engine is 2. If both the UI and GPU /// task runner tasks finish within one frame interval, the /// pipeline depth is one. If the UI thread happens to be - /// working on a frame when the GPU thread is still not done + /// working on a frame when the raster thread is still not done /// with the previous frame, the pipeline depth is 2. When the /// pipeline depth changes from 1 to 2, animations and UI /// interactions that cause the generation of the new layer tree diff --git a/shell/common/persistent_cache.cc b/shell/common/persistent_cache.cc index 66690971ace46..0537905ed22f0 100644 --- a/shell/common/persistent_cache.cc +++ b/shell/common/persistent_cache.cc @@ -20,11 +20,12 @@ namespace flutter { std::string PersistentCache::cache_base_path_; +std::string PersistentCache::asset_path_; std::mutex PersistentCache::instance_mutex_; std::unique_ptr PersistentCache::gPersistentCache; -static std::string SkKeyToFilePath(const SkData& data) { +std::string PersistentCache::SkKeyToFilePath(const SkData& data) { if (data.data() == nullptr || data.size() == 0) { return ""; } @@ -90,7 +91,7 @@ static std::shared_ptr MakeCacheDirectory( std::vector components = { "flutter_engine", GetFlutterEngineVersion(), "skia", GetSkiaVersion()}; if (cache_sksl) { - components.push_back("sksl"); + components.push_back(PersistentCache::kSkSLSubdirName); } return std::make_shared( CreateDirectory(cache_base_dir, components, @@ -105,9 +106,6 @@ static std::shared_ptr MakeCacheDirectory( std::vector PersistentCache::LoadSkSLs() { TRACE_EVENT0("flutter", "PersistentCache::LoadSkSLs"); std::vector result; - if (!IsValid()) { - return result; - } fml::FileVisitor visitor = [&result](const fml::UniqueFD& directory, const std::string& filename) { std::pair decode_result = fml::Base32Decode(filename); @@ -126,7 +124,25 @@ std::vector PersistentCache::LoadSkSLs() { } return true; }; - fml::VisitFiles(*sksl_cache_directory_, visitor); + + // Only visit sksl_cache_directory_ if this persistent cache is valid. + // However, we'd like to continue visit the asset dir even if this persistent + // cache is invalid. + if (IsValid()) { + fml::VisitFiles(*sksl_cache_directory_, visitor); + } + + fml::UniqueFD root_asset_dir = fml::OpenDirectory(asset_path_.c_str(), false, + fml::FilePermission::kRead); + fml::UniqueFD sksl_asset_dir = + fml::OpenDirectoryReadOnly(root_asset_dir, kSkSLSubdirName); + if (sksl_asset_dir.is_valid()) { + FML_LOG(INFO) << "Found sksl asset directory. Loading SkSLs from it..."; + fml::VisitFiles(sksl_asset_dir, visitor); + } else { + FML_LOG(INFO) << "No sksl asset directory found."; + } + return result; } @@ -283,4 +299,9 @@ fml::RefPtr PersistentCache::GetWorkerTaskRunner() const { return worker; } +void PersistentCache::UpdateAssetPath(const std::string& path) { + FML_LOG(INFO) << "PersistentCache::UpdateAssetPath: " << path; + asset_path_ = path; +} + } // namespace flutter diff --git a/shell/common/persistent_cache.h b/shell/common/persistent_cache.h index 35d8f08f8598a..5cfb0afb75bdb 100644 --- a/shell/common/persistent_cache.h +++ b/shell/common/persistent_cache.h @@ -32,8 +32,16 @@ class PersistentCache : public GrContextOptions::PersistentCache { static PersistentCache* GetCacheForProcess(); static void ResetCacheForProcess(); + // This must be called before |GetCacheForProcess|. Otherwise, it won't + // affect the cache directory returned by |GetCacheForProcess|. static void SetCacheDirectoryPath(std::string path); + // Convert a binary SkData key into a Base32 encoded string. + // + // This is used to specify persistent cache filenames and service protocol + // json keys. + static std::string SkKeyToFilePath(const SkData& data); + ~PersistentCache() override; void AddWorkerTaskRunner(fml::RefPtr task_runner); @@ -57,12 +65,18 @@ class PersistentCache : public GrContextOptions::PersistentCache { /// Load all the SkSL shader caches in the right directory. std::vector LoadSkSLs(); + /// Update the asset path from which PersistentCache can load SkLSs. + static void UpdateAssetPath(const std::string& path); + static bool cache_sksl() { return cache_sksl_; } static void SetCacheSkSL(bool value); static void MarkStrategySet() { strategy_set_ = true; } + static constexpr char kSkSLSubdirName[] = "sksl"; + private: static std::string cache_base_path_; + static std::string asset_path_; static std::mutex instance_mutex_; static std::unique_ptr gPersistentCache; diff --git a/shell/common/persistent_cache_unittests.cc b/shell/common/persistent_cache_unittests.cc index 4948a3359bc69..597727af3b8ae 100644 --- a/shell/common/persistent_cache_unittests.cc +++ b/shell/common/persistent_cache_unittests.cc @@ -10,6 +10,7 @@ #include "flutter/flow/layers/picture_layer.h" #include "flutter/fml/command_line.h" #include "flutter/fml/file.h" +#include "flutter/fml/log_settings.h" #include "flutter/fml/unique_fd.h" #include "flutter/shell/common/persistent_cache.h" #include "flutter/shell/common/shell_test.h" @@ -123,5 +124,99 @@ TEST_F(ShellTest, CacheSkSLWorks) { DestroyShell(std::move(shell)); } +static void CheckTextSkData(sk_sp data, const std::string& expected) { + std::string data_string(reinterpret_cast(data->bytes()), + data->size()); + ASSERT_EQ(data_string, expected); +} + +void ResetAssetPath() { + PersistentCache::UpdateAssetPath("some_path_that_does_not_exist"); + ASSERT_EQ(PersistentCache::GetCacheForProcess()->LoadSkSLs().size(), 0u); +} + +void CheckTwoSkSLsAreLoaded() { + auto shaders = PersistentCache::GetCacheForProcess()->LoadSkSLs(); + ASSERT_EQ(shaders.size(), 2u); +} + +TEST_F(ShellTest, CanLoadSkSLsFromAsset) { + // Avoid polluting unit tests output by hiding INFO level logging. + fml::LogSettings warning_only = {fml::LOG_WARNING}; + fml::ScopedSetLogSettings scoped_set_log_settings(warning_only); + + // Create an empty shell to test its service protocol handlers. + auto empty_settings = CreateSettingsForFixture(); + auto empty_config = RunConfiguration::InferFromSettings(empty_settings); + std::unique_ptr empty_shell = CreateShell(empty_settings); + + // Temp dir for the asset. + fml::ScopedTemporaryDirectory asset_dir; + fml::UniqueFD sksl_asset_dir = + fml::OpenDirectory(asset_dir.fd(), PersistentCache::kSkSLSubdirName, true, + fml::FilePermission::kReadWrite); + + // The SkSL filenames are Base32 encoded strings. "IE" is the encoding of "A" + // and "II" is the encoding of "B". + const std::string kFileNames[2] = {"IE", "II"}; + const std::string kFileData[2] = {"x", "y"}; + + // Prepare 2 SkSL files in the asset directory. + for (int i = 0; i < 2; i += 1) { + auto data = std::make_unique( + std::vector{kFileData[i].begin(), kFileData[i].end()}); + fml::WriteAtomically(sksl_asset_dir, kFileNames[i].c_str(), *data); + } + + // 1st, test that RunConfiguration::InferFromSettings sets the path. + ResetAssetPath(); + auto settings = CreateSettingsForFixture(); + settings.assets_path = asset_dir.path(); + RunConfiguration::InferFromSettings(settings); + CheckTwoSkSLsAreLoaded(); + + // 2nd, test that Shell::OnServiceProtocolSetAssetBundlePath sets the path. + ResetAssetPath(); + ServiceProtocol::Handler::ServiceProtocolMap params; + rapidjson::Document document; + params["assetDirectory"] = asset_dir.path(); + OnServiceProtocol( + empty_shell.get(), ShellTest::ServiceProtocolEnum::kSetAssetBundlePath, + empty_shell->GetTaskRunners().GetUITaskRunner(), params, document); + CheckTwoSkSLsAreLoaded(); + + // 3rd, test that Shell::OnServiceProtocolRunInView sets the path. + ResetAssetPath(); + params["assetDirectory"] = asset_dir.path(); + params["mainScript"] = "no_such_script.dart"; + OnServiceProtocol( + empty_shell.get(), ShellTest::ServiceProtocolEnum::kSetAssetBundlePath, + empty_shell->GetTaskRunners().GetUITaskRunner(), params, document); + CheckTwoSkSLsAreLoaded(); + + // 4th, test the content of the SkSLs in the asset. + { + auto shaders = PersistentCache::GetCacheForProcess()->LoadSkSLs(); + ASSERT_EQ(shaders.size(), 2u); + + // Make sure that the 2 shaders are sorted by their keys. Their keys should + // be "A" and "B" (decoded from "II" and "IE"). + if (shaders[0].first->bytes()[0] == 'B') { + std::swap(shaders[0], shaders[1]); + } + + CheckTextSkData(shaders[0].first, "A"); + CheckTextSkData(shaders[1].first, "B"); + CheckTextSkData(shaders[0].second, "x"); + CheckTextSkData(shaders[1].second, "y"); + } + + // Cleanup. + DestroyShell(std::move(empty_shell)); + fml::UnlinkFile(sksl_asset_dir, kFileNames[0].c_str()); + fml::UnlinkFile(sksl_asset_dir, kFileNames[1].c_str()); + fml::UnlinkDirectory(asset_dir.fd(), PersistentCache::kSkSLSubdirName); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/pipeline.h b/shell/common/pipeline.h index 587c81ff6fbc2..296d9bf4a87ab 100644 --- a/shell/common/pipeline.h +++ b/shell/common/pipeline.h @@ -111,16 +111,22 @@ class Pipeline : public fml::RefCountedThreadSafe> { GetNextPipelineTraceID()}; // trace id } - // Pushes task to the front of the pipeline. - // - // If we exceed the depth completing this continuation, we drop the - // last frame to preserve the depth of the pipeline. - // - // Note: Use |Pipeline::Produce| where possible. This should only be - // used to en-queue high-priority resources. - ProducerContinuation ProduceToFront() { + // Create a `ProducerContinuation` that will only push the task if the queue + // is empty. + // Prefer using |Produce|. ProducerContinuation returned by this method + // doesn't guarantee that the frame will be rendered. + ProducerContinuation ProduceIfEmpty() { + if (!empty_.TryWait()) { + return {}; + } + ++inflight_; + FML_TRACE_COUNTER("flutter", "Pipeline Depth", + reinterpret_cast(this), // + "frames in flight", inflight_.load() // + ); + return ProducerContinuation{ - std::bind(&Pipeline::ProducerCommitFront, this, std::placeholders::_1, + std::bind(&Pipeline::ProducerCommitIfEmpty, this, std::placeholders::_1, std::placeholders::_2), // continuation GetNextPipelineTraceID()}; // trace id } @@ -181,13 +187,16 @@ class Pipeline : public fml::RefCountedThreadSafe> { available_.Signal(); } - void ProducerCommitFront(ResourcePtr resource, size_t trace_id) { + void ProducerCommitIfEmpty(ResourcePtr resource, size_t trace_id) { { std::scoped_lock lock(queue_mutex_); - queue_.emplace_front(std::move(resource), trace_id); - while (queue_.size() > depth_) { - queue_.pop_back(); + if (!queue_.empty()) { + // Bail if the queue is not empty, opens up spaces to produce other + // frames. + empty_.Signal(); + return; } + queue_.emplace_back(std::move(resource), trace_id); } // Ensure the queue mutex is not held as that would be a pessimization. diff --git a/shell/common/pipeline_unittests.cc b/shell/common/pipeline_unittests.cc index ada908953b45a..76fdc85867ee7 100644 --- a/shell/common/pipeline_unittests.cc +++ b/shell/common/pipeline_unittests.cc @@ -89,46 +89,34 @@ TEST(PipelineTest, PushingMultiProcessesInOrder) { ASSERT_EQ(consume_result_2, PipelineConsumeResult::Done); } -TEST(PipelineTest, PushingToFrontOverridesOrder) { +TEST(PipelineTest, ProduceIfEmptyDoesNotConsumeWhenQueueIsNotEmpty) { const int depth = 2; fml::RefPtr pipeline = fml::MakeRefCounted(depth); Continuation continuation_1 = pipeline->Produce(); - Continuation continuation_2 = pipeline->ProduceToFront(); + Continuation continuation_2 = pipeline->ProduceIfEmpty(); const int test_val_1 = 1, test_val_2 = 2; continuation_1.Complete(std::make_unique(test_val_1)); continuation_2.Complete(std::make_unique(test_val_2)); PipelineConsumeResult consume_result_1 = pipeline->Consume( - [&test_val_2](std::unique_ptr v) { ASSERT_EQ(*v, test_val_2); }); - ASSERT_EQ(consume_result_1, PipelineConsumeResult::MoreAvailable); - - PipelineConsumeResult consume_result_2 = pipeline->Consume( [&test_val_1](std::unique_ptr v) { ASSERT_EQ(*v, test_val_1); }); - ASSERT_EQ(consume_result_2, PipelineConsumeResult::Done); + ASSERT_EQ(consume_result_1, PipelineConsumeResult::Done); } -TEST(PipelineTest, PushingToFrontDropsLastResource) { - const int depth = 2; +TEST(PipelineTest, ProduceIfEmptySuccessfulIfQueueIsEmpty) { + const int depth = 1; fml::RefPtr pipeline = fml::MakeRefCounted(depth); - Continuation continuation_1 = pipeline->Produce(); - Continuation continuation_2 = pipeline->Produce(); - Continuation continuation_3 = pipeline->ProduceToFront(); + Continuation continuation_1 = pipeline->ProduceIfEmpty(); - const int test_val_1 = 1, test_val_2 = 2, test_val_3 = 3; + const int test_val_1 = 1; continuation_1.Complete(std::make_unique(test_val_1)); - continuation_2.Complete(std::make_unique(test_val_2)); - continuation_3.Complete(std::make_unique(test_val_3)); PipelineConsumeResult consume_result_1 = pipeline->Consume( - [&test_val_3](std::unique_ptr v) { ASSERT_EQ(*v, test_val_3); }); - ASSERT_EQ(consume_result_1, PipelineConsumeResult::MoreAvailable); - - PipelineConsumeResult consume_result_2 = pipeline->Consume( [&test_val_1](std::unique_ptr v) { ASSERT_EQ(*v, test_val_1); }); - ASSERT_EQ(consume_result_2, PipelineConsumeResult::Done); + ASSERT_EQ(consume_result_1, PipelineConsumeResult::Done); } } // namespace testing diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index f96dc0a945994..9fa42ae17d22e 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -69,7 +69,7 @@ void PlatformView::NotifyCreated() { auto* platform_view = this; fml::ManualResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), [platform_view, &surface, &latch]() { + task_runners_.GetRasterTaskRunner(), [platform_view, &surface, &latch]() { surface = platform_view->CreateRenderingSurface(); latch.Signal(); }); diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index f80f7519dcddf..15d3edac4b210 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -172,7 +172,7 @@ class PlatformView { /// Flutter layer tree. All textures must have a unique /// identifier. When the rasterizer encounters an external /// texture within its hierarchy, it gives the embedder a chance - /// to update that texture on the GPU thread before it + /// to update that texture on the raster thread before it /// composites the same on-screen. /// /// @param[in] texture The texture that is being updated by the embedder @@ -352,10 +352,11 @@ class PlatformView { //---------------------------------------------------------------------------- /// @brief Used by embedders to specify the updated viewport metrics. In - /// response to this call, on the GPU thread, the rasterizer may - /// need to be reconfigured to the updated viewport dimensions. On - /// the UI thread, the framework may need to start generating a - /// new frame for the updated viewport metrics as well. + /// response to this call, on the raster thread, the rasterizer + /// may need to be reconfigured to the updated viewport + /// dimensions. On the UI thread, the framework may need to start + /// generating a new frame for the updated viewport metrics as + /// well. /// /// @param[in] metrics The updated viewport metrics. /// @@ -488,7 +489,7 @@ class PlatformView { /// textures must have a unique identifier. When the /// rasterizer encounters an external texture within its /// hierarchy, it gives the embedder a chance to update that - /// texture on the GPU thread before it composites the same + /// texture on the raster thread before it composites the same /// on-screen. /// /// @attention This method must only be called once per texture. When the diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index acad5ada8510a..ce456925c69ac 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -8,6 +8,8 @@ #include +#include "flutter/fml/time/time_delta.h" +#include "flutter/fml/time/time_point.h" #include "third_party/skia/include/core/SkEncodedImageFormat.h" #include "third_party/skia/include/core/SkImageEncoder.h" #include "third_party/skia/include/core/SkPictureRecorder.h" @@ -69,9 +71,9 @@ void Rasterizer::Setup(std::unique_ptr surface) { if (surface_->GetExternalViewEmbedder()) { const auto platform_id = task_runners_.GetPlatformTaskRunner()->GetTaskQueueId(); - const auto gpu_id = task_runners_.GetGPUTaskRunner()->GetTaskQueueId(); - gpu_thread_merger_ = - fml::MakeRefCounted(platform_id, gpu_id); + const auto gpu_id = task_runners_.GetRasterTaskRunner()->GetTaskQueueId(); + raster_thread_merger_ = + fml::MakeRefCounted(platform_id, gpu_id); } } @@ -111,11 +113,12 @@ void Rasterizer::DrawLastLayerTree() { void Rasterizer::Draw(fml::RefPtr> pipeline) { TRACE_EVENT0("flutter", "GPURasterizer::Draw"); - if (gpu_thread_merger_ && !gpu_thread_merger_->IsOnRasterizingThread()) { + if (raster_thread_merger_ && + !raster_thread_merger_->IsOnRasterizingThread()) { // we yield and let this frame be serviced on the right thread. return; } - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); RasterStatus raster_status = RasterStatus::kFailed; Pipeline::Consumer consumer = @@ -127,18 +130,27 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // if the raster status is to resubmit the frame, we push the frame to the // front of the queue and also change the consume status to more available. if (raster_status == RasterStatus::kResubmit) { - auto front_continuation = pipeline->ProduceToFront(); + auto front_continuation = pipeline->ProduceIfEmpty(); front_continuation.Complete(std::move(resubmitted_layer_tree_)); - consume_result = PipelineConsumeResult::MoreAvailable; } else if (raster_status == RasterStatus::kEnqueuePipeline) { consume_result = PipelineConsumeResult::MoreAvailable; } + // Merging the thread as we know the next `Draw` should be run on the platform + // thread. + if (raster_status == RasterStatus::kResubmit) { + auto* external_view_embedder = surface_->GetExternalViewEmbedder(); + // We know only the `external_view_embedder` can + // causes|RasterStatus::kResubmit|. Check to make sure. + FML_DCHECK(external_view_embedder != nullptr); + external_view_embedder->EndFrame(raster_thread_merger_); + } + // Consume as many pipeline items as possible. But yield the event loop // between successive tries. switch (consume_result) { case PipelineConsumeResult::MoreAvailable: { - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [weak_this = weak_factory_.GetWeakPtr(), pipeline]() { if (weak_this) { weak_this->Draw(pipeline); @@ -233,13 +245,14 @@ sk_sp Rasterizer::ConvertToRasterImage(sk_sp image) { RasterStatus Rasterizer::DoDraw( std::unique_ptr layer_tree) { - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); if (!layer_tree || !surface_) { return RasterStatus::kFailed; } FrameTiming timing; + const fml::TimePoint frame_target_time = layer_tree->target_time(); timing.Set(FrameTiming::kBuildStart, layer_tree->build_start()); timing.Set(FrameTiming::kBuildFinish, layer_tree->build_finish()); timing.Set(FrameTiming::kRasterStart, fml::TimePoint::Now()); @@ -265,9 +278,36 @@ RasterStatus Rasterizer::DoDraw( // TODO(liyuqian): in Fuchsia, the rasterization doesn't finish when // Rasterizer::DoDraw finishes. Future work is needed to adapt the timestamp // for Fuchsia to capture SceneUpdateContext::ExecutePaintTasks. - timing.Set(FrameTiming::kRasterFinish, fml::TimePoint::Now()); + const auto raster_finish_time = fml::TimePoint::Now(); + timing.Set(FrameTiming::kRasterFinish, raster_finish_time); delegate_.OnFrameRasterized(timing); + if (raster_finish_time > frame_target_time) { + fml::TimePoint latest_frame_target_time = + delegate_.GetLatestFrameTargetTime(); + const auto frame_budget_millis = delegate_.GetFrameBudget().count(); + if (latest_frame_target_time < raster_finish_time) { + latest_frame_target_time = + latest_frame_target_time + + fml::TimeDelta::FromMillisecondsF(frame_budget_millis); + } + const auto frame_lag = + (latest_frame_target_time - frame_target_time).ToMillisecondsF(); + const int vsync_transitions_missed = round(frame_lag / frame_budget_millis); + fml::tracing::TraceEventAsyncComplete( + "flutter", // category + "SceneDisplayLag", // name + raster_finish_time, // begin_time + latest_frame_target_time, // end_time + "frame_target_time", // arg_key_1 + frame_target_time, // arg_val_1 + "current_frame_target_time", // arg_key_2 + latest_frame_target_time, // arg_val_2 + "vsync_transitions_missed", // arg_key_3 + vsync_transitions_missed // arg_val_3 + ); + } + // Pipeline pressure is applied from a couple of places: // rasterizer: When there are more items as of the time of Consume. // animator (via shell): Frame gets produces every vsync. @@ -284,9 +324,9 @@ RasterStatus Rasterizer::DoDraw( // - |Draw| for B yields as its on the wrong thread. // This enqueue ensures that we attempt to consume from the right // thread one more time after un-merge. - if (gpu_thread_merger_) { - if (gpu_thread_merger_->DecrementLease() == - fml::GpuThreadStatus::kUnmergedNow) { + if (raster_thread_merger_) { + if (raster_thread_merger_->DecrementLease() == + fml::RasterThreadStatus::kUnmergedNow) { return RasterStatus::kEnqueuePipeline; } } @@ -335,7 +375,7 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { root_surface_transformation, // root surface transformation true, // instrumentation enabled frame->supports_readback(), // surface supports pixel reads - gpu_thread_merger_ // thread merger + raster_thread_merger_ // thread merger ); if (compositor_frame) { @@ -343,9 +383,17 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { if (raster_status == RasterStatus::kFailed) { return raster_status; } - frame->Submit(); if (external_view_embedder != nullptr) { - external_view_embedder->SubmitFrame(surface_->GetContext()); + external_view_embedder->SubmitFrame(surface_->GetContext(), + root_surface_canvas); + // The external view embedder may mutate the root surface canvas while + // submitting the frame. + // Therefore, submit the final frame after asking the external view + // embedder to submit the frame. + frame->Submit(); + external_view_embedder->FinishFrame(); + } else { + frame->Submit(); } FireNextFrameCallbackIfPresent(); diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index fcc1e356c6399..dac22644db0ab 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -13,9 +13,11 @@ #include "flutter/flow/compositor_context.h" #include "flutter/flow/layers/layer_tree.h" #include "flutter/fml/closure.h" -#include "flutter/fml/gpu_thread_merger.h" #include "flutter/fml/memory/weak_ptr.h" +#include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/fml/time/time_delta.h" +#include "flutter/fml/time/time_point.h" #include "flutter/lib/ui/snapshot_delegate.h" #include "flutter/shell/common/pipeline.h" #include "flutter/shell/common/surface.h" @@ -67,6 +69,10 @@ class Rasterizer final : public SnapshotDelegate { /// Time limit for a smooth frame. See `Engine::GetDisplayRefreshRate`. virtual fml::Milliseconds GetFrameBudget() = 0; + + /// Target time for the latest frame. See also `Shell::OnAnimatorBeginFrame` + /// for when this time gets updated. + virtual fml::TimePoint GetLatestFrameTargetTime() const = 0; }; // TODO(dnfield): remove once embedders have caught up. @@ -75,6 +81,11 @@ class Rasterizer final : public SnapshotDelegate { fml::Milliseconds GetFrameBudget() override { return fml::kDefaultFrameBudget; } + // Returning a time in the past so we don't add additional trace + // events when exceeding the frame budget for other embedders. + fml::TimePoint GetLatestFrameTargetTime() const override { + return fml::TimePoint::FromEpochDelta(fml::TimeDelta::Zero()); + } }; //---------------------------------------------------------------------------- @@ -220,8 +231,8 @@ class Rasterizer final : public SnapshotDelegate { //---------------------------------------------------------------------------- /// @brief Takes the next item from the layer tree pipeline and executes - /// the GPU thread frame workload for that pipeline item to render - /// a frame on the on-screen surface. + /// the raster thread frame workload for that pipeline item to + /// render a frame on the on-screen surface. /// /// Why does the draw call take a layer tree pipeline and not the /// layer tree directly? @@ -420,7 +431,7 @@ class Rasterizer final : public SnapshotDelegate { bool user_override_resource_cache_bytes_; std::optional max_cache_bytes_; fml::WeakPtrFactory weak_factory_; - fml::RefPtr gpu_thread_merger_; + fml::RefPtr raster_thread_merger_; // |SnapshotDelegate| sk_sp MakeRasterSnapshot(sk_sp picture, diff --git a/shell/common/run_configuration.cc b/shell/common/run_configuration.cc index de0aa1adc9126..e87c1f0bb5717 100644 --- a/shell/common/run_configuration.cc +++ b/shell/common/run_configuration.cc @@ -10,6 +10,7 @@ #include "flutter/fml/file.h" #include "flutter/fml/unique_fd.h" #include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/persistent_cache.h" namespace flutter { @@ -26,6 +27,7 @@ RunConfiguration RunConfiguration::InferFromSettings( asset_manager->PushBack( std::make_unique(fml::OpenDirectory( settings.assets_path.c_str(), false, fml::FilePermission::kRead))); + PersistentCache::UpdateAssetPath(settings.assets_path); return {IsolateConfiguration::InferFromSettings(settings, asset_manager, io_worker), diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 3a3f9cfa901ef..16a76bb9a9eaa 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -30,6 +30,7 @@ #include "rapidjson/writer.h" #include "third_party/dart/runtime/include/dart_tools_api.h" #include "third_party/skia/include/core/SkGraphics.h" +#include "third_party/skia/include/utils/SkBase64.h" #include "third_party/tonic/common/log.h" namespace flutter { @@ -55,16 +56,16 @@ std::unique_ptr Shell::CreateShellOnPlatformThread( auto shell = std::unique_ptr(new Shell(std::move(vm), task_runners, settings)); - // Create the rasterizer on the GPU thread. + // Create the rasterizer on the raster thread. std::promise> rasterizer_promise; auto rasterizer_future = rasterizer_promise.get_future(); std::promise> snapshot_delegate_promise; auto snapshot_delegate_future = snapshot_delegate_promise.get_future(); fml::TaskRunner::RunNowOrPostTask( - task_runners.GetGPUTaskRunner(), [&rasterizer_promise, // - &snapshot_delegate_promise, - on_create_rasterizer, // - shell = shell.get() // + task_runners.GetRasterTaskRunner(), [&rasterizer_promise, // + &snapshot_delegate_promise, + on_create_rasterizer, // + shell = shell.get() // ]() { TRACE_EVENT0("flutter", "ShellSetupGPUSubsystem"); std::unique_ptr rasterizer(on_create_rasterizer(*shell)); @@ -180,6 +181,16 @@ static void RecordStartupTimestamp() { } } +static void Tokenize(const std::string& input, + std::vector* results, + char delimiter) { + std::istringstream ss(input); + std::string token; + while (std::getline(ss, token, delimiter)) { + results->push_back(token); + } +} + // Though there can be multiple shells, some settings apply to all components in // the process. These have to be setup before the shell or any of its // sub-components can be initialized. In a perfect world, this would be empty. @@ -205,6 +216,12 @@ static void PerformInitializationTasks(const Settings& settings) { InitSkiaEventTracer(settings.trace_skia); } + if (!settings.trace_whitelist.empty()) { + std::vector prefixes; + Tokenize(settings.trace_whitelist, &prefixes, ','); + fml::tracing::TraceSetWhitelist(prefixes); + } + if (!settings.skia_deterministic_rendering_on_cpu) { SkGraphics::Init(); } else { @@ -319,11 +336,11 @@ Shell::Shell(DartVMRef vm, TaskRunners task_runners, Settings settings) FML_DCHECK(task_runners_.IsValid()); FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); - // Generate a WeakPtrFactory for use with the GPU thread. This does not need - // to wait on a latch because it can only ever be used from the GPU thread - // from this class, so we have ordering guarantees. + // Generate a WeakPtrFactory for use with the raster thread. This does not + // need to wait on a latch because it can only ever be used from the raster + // thread from this class, so we have ordering guarantees. fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), fml::MakeCopyable([this]() mutable { + task_runners_.GetRasterTaskRunner(), fml::MakeCopyable([this]() mutable { this->weak_factory_gpu_ = std::make_unique>(this); })); @@ -331,11 +348,11 @@ Shell::Shell(DartVMRef vm, TaskRunners task_runners, Settings settings) // Install service protocol handlers. service_protocol_handlers_[ServiceProtocol::kScreenshotExtensionName] = { - task_runners_.GetGPUTaskRunner(), + task_runners_.GetRasterTaskRunner(), std::bind(&Shell::OnServiceProtocolScreenshot, this, std::placeholders::_1, std::placeholders::_2)}; service_protocol_handlers_[ServiceProtocol::kScreenshotSkpExtensionName] = { - task_runners_.GetGPUTaskRunner(), + task_runners_.GetRasterTaskRunner(), std::bind(&Shell::OnServiceProtocolScreenshotSKP, this, std::placeholders::_1, std::placeholders::_2)}; service_protocol_handlers_[ServiceProtocol::kRunInViewExtensionName] = { @@ -357,6 +374,10 @@ Shell::Shell(DartVMRef vm, TaskRunners task_runners, Settings settings) task_runners_.GetUITaskRunner(), std::bind(&Shell::OnServiceProtocolGetDisplayRefreshRate, this, std::placeholders::_1, std::placeholders::_2)}; + service_protocol_handlers_[ServiceProtocol::kGetSkSLsExtensionName] = { + task_runners_.GetIOTaskRunner(), + std::bind(&Shell::OnServiceProtocolGetSkSLs, this, std::placeholders::_1, + std::placeholders::_2)}; } Shell::~Shell() { @@ -376,7 +397,7 @@ Shell::~Shell() { ui_latch.Wait(); fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), + task_runners_.GetRasterTaskRunner(), fml::MakeCopyable([rasterizer = std::move(rasterizer_), weak_factory_gpu = std::move(weak_factory_gpu_), &gpu_latch]() mutable { @@ -420,7 +441,7 @@ void Shell::NotifyLowMemoryWarning() const { // running. ::Dart_NotifyLowMemory(); - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr()]() { if (rasterizer) { rasterizer->NotifyLowMemoryWarning(); @@ -584,8 +605,8 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { // setup/suspension of all activities that may be interacting with the GPU in // a synchronous fashion. fml::AutoResetWaitableEvent latch; - auto gpu_task = - fml::MakeCopyable([& waiting_for_first_frame = waiting_for_first_frame_, + auto raster_task = + fml::MakeCopyable([&waiting_for_first_frame = waiting_for_first_frame_, rasterizer = rasterizer_->GetWeakPtr(), // surface = std::move(surface), // &latch]() mutable { @@ -602,30 +623,31 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { // The normal flow executed by this method is that the platform thread is // starting the sequence and waiting on the latch. Later the UI thread posts - // gpu_task to the GPU thread which signals the latch. If the GPU the and - // platform threads are the same this results in a deadlock as the gpu_task - // will never be posted to the plaform/gpu thread that is blocked on a latch. - // To avoid the described deadlock, if the gpu and the platform threads are - // the same, should_post_gpu_task will be false, and then instead of posting a - // task to the gpu thread, the ui thread just signals the latch and the - // platform/gpu thread follows with executing gpu_task. - bool should_post_gpu_task = - task_runners_.GetGPUTaskRunner() != task_runners_.GetPlatformTaskRunner(); - - auto ui_task = [engine = engine_->GetWeakPtr(), // - gpu_task_runner = task_runners_.GetGPUTaskRunner(), // - gpu_task, should_post_gpu_task, + // raster_task to the raster thread which signals the latch. If the raster and + // the platform threads are the same this results in a deadlock as the + // raster_task will never be posted to the plaform/raster thread that is + // blocked on a latch. To avoid the described deadlock, if the raster and the + // platform threads are the same, should_post_raster_task will be false, and + // then instead of posting a task to the raster thread, the ui thread just + // signals the latch and the platform/raster thread follows with executing + // raster_task. + bool should_post_raster_task = task_runners_.GetRasterTaskRunner() != + task_runners_.GetPlatformTaskRunner(); + + auto ui_task = [engine = engine_->GetWeakPtr(), // + raster_task_runner = task_runners_.GetRasterTaskRunner(), // + raster_task, should_post_raster_task, &latch // ] { if (engine) { engine->OnOutputSurfaceCreated(); } - // Step 2: Next, tell the GPU thread that it should create a surface for its - // rasterizer. - if (should_post_gpu_task) { - fml::TaskRunner::RunNowOrPostTask(gpu_task_runner, gpu_task); + // Step 2: Next, tell the raster thread that it should create a surface for + // its rasterizer. + if (should_post_raster_task) { + fml::TaskRunner::RunNowOrPostTask(raster_task_runner, raster_task); } else { - // See comment on should_post_gpu_task, in this case we just unblock + // See comment on should_post_raster_task, in this case we just unblock // the platform thread. latch.Signal(); } @@ -653,11 +675,11 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { fml::TaskRunner::RunNowOrPostTask(task_runners_.GetIOTaskRunner(), io_task); latch.Wait(); - if (!should_post_gpu_task) { - // See comment on should_post_gpu_task, in this case the gpu_task + if (!should_post_raster_task) { + // See comment on should_post_raster_task, in this case the raster_task // wasn't executed, and we just run it here as the platform thread - // is the GPU thread. - gpu_task(); + // is the raster thread. + raster_task(); } } @@ -685,9 +707,9 @@ void Shell::OnPlatformViewDestroyed() { latch.Signal(); }; - auto gpu_task = [rasterizer = rasterizer_->GetWeakPtr(), - io_task_runner = task_runners_.GetIOTaskRunner(), - io_task]() { + auto raster_task = [rasterizer = rasterizer_->GetWeakPtr(), + io_task_runner = task_runners_.GetIOTaskRunner(), + io_task]() { if (rasterizer) { rasterizer->Teardown(); } @@ -697,28 +719,29 @@ void Shell::OnPlatformViewDestroyed() { // The normal flow executed by this method is that the platform thread is // starting the sequence and waiting on the latch. Later the UI thread posts - // gpu_task to the GPU thread triggers signaling the latch(on the IO thread). - // If the GPU the and platform threads are the same this results in a deadlock - // as the gpu_task will never be posted to the plaform/gpu thread that is - // blocked on a latch. To avoid the described deadlock, if the gpu and the - // platform threads are the same, should_post_gpu_task will be false, and then - // instead of posting a task to the gpu thread, the ui thread just signals the - // latch and the platform/gpu thread follows with executing gpu_task. - bool should_post_gpu_task = - task_runners_.GetGPUTaskRunner() != task_runners_.GetPlatformTaskRunner(); + // raster_task to the raster thread triggers signaling the latch(on the IO + // thread). If the raster and the platform threads are the same this results + // in a deadlock as the raster_task will never be posted to the plaform/raster + // thread that is blocked on a latch. To avoid the described deadlock, if the + // raster and the platform threads are the same, should_post_raster_task will + // be false, and then instead of posting a task to the raster thread, the ui + // thread just signals the latch and the platform/raster thread follows with + // executing raster_task. + bool should_post_raster_task = task_runners_.GetRasterTaskRunner() != + task_runners_.GetPlatformTaskRunner(); auto ui_task = [engine = engine_->GetWeakPtr(), - gpu_task_runner = task_runners_.GetGPUTaskRunner(), gpu_task, - should_post_gpu_task, &latch]() { + raster_task_runner = task_runners_.GetRasterTaskRunner(), + raster_task, should_post_raster_task, &latch]() { if (engine) { engine->OnOutputSurfaceDestroyed(); } - // Step 1: Next, tell the GPU thread that its rasterizer should suspend + // Step 1: Next, tell the raster thread that its rasterizer should suspend // access to the underlying surface. - if (should_post_gpu_task) { - fml::TaskRunner::RunNowOrPostTask(gpu_task_runner, gpu_task); + if (should_post_raster_task) { + fml::TaskRunner::RunNowOrPostTask(raster_task_runner, raster_task); } else { - // See comment on should_post_gpu_task, in this case we just unblock + // See comment on should_post_raster_task, in this case we just unblock // the platform thread. latch.Signal(); } @@ -728,11 +751,11 @@ void Shell::OnPlatformViewDestroyed() { // surface is about to go away. fml::TaskRunner::RunNowOrPostTask(task_runners_.GetUITaskRunner(), ui_task); latch.Wait(); - if (!should_post_gpu_task) { - // See comment on should_post_gpu_task, in this case the gpu_task + if (!should_post_raster_task) { + // See comment on should_post_raster_task, in this case the raster_task // wasn't executed, and we just run it here as the platform thread - // is the GPU thread. - gpu_task(); + // is the raster thread. + raster_task(); latch.Wait(); } } @@ -745,7 +768,7 @@ void Shell::OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) { // This is the formula Android uses. // https://android.googlesource.com/platform/frameworks/base/+/master/libs/hwui/renderthread/CacheManager.cpp#41 size_t max_bytes = metrics.physical_width * metrics.physical_height * 12 * 4; - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), max_bytes] { if (rasterizer) { rasterizer->SetResourceCacheMaxBytes(max_bytes, false); @@ -838,7 +861,7 @@ void Shell::OnPlatformViewRegisterTexture( FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), texture] { if (rasterizer) { if (auto* registry = rasterizer->GetTextureRegistry()) { @@ -853,7 +876,7 @@ void Shell::OnPlatformViewUnregisterTexture(int64_t texture_id) { FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), texture_id]() { if (rasterizer) { if (auto* registry = rasterizer->GetTextureRegistry()) { @@ -869,7 +892,7 @@ void Shell::OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) { FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); // Tell the rasterizer that one of its textures has a new frame available. - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), texture_id]() { auto* registry = rasterizer->GetTextureRegistry(); @@ -899,7 +922,7 @@ void Shell::OnPlatformViewSetNextFrameCallback(const fml::closure& closure) { FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), closure = closure]() { if (rasterizer) { rasterizer->SetNextFrameCallback(std::move(closure)); @@ -908,12 +931,17 @@ void Shell::OnPlatformViewSetNextFrameCallback(const fml::closure& closure) { } // |Animator::Delegate| -void Shell::OnAnimatorBeginFrame(fml::TimePoint frame_time) { +void Shell::OnAnimatorBeginFrame(fml::TimePoint frame_target_time) { FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); + // record the target time for use by rasterizer. + { + std::scoped_lock time_recorder_lock(time_recorder_mutex_); + latest_frame_target_time_.emplace(frame_target_time); + } if (engine_) { - engine_->BeginFrame(frame_time); + engine_->BeginFrame(frame_target_time); } } @@ -931,8 +959,8 @@ void Shell::OnAnimatorNotifyIdle(int64_t deadline) { void Shell::OnAnimatorDraw(fml::RefPtr> pipeline) { FML_DCHECK(is_setup_); - task_runners_.GetGPUTaskRunner()->PostTask( - [& waiting_for_first_frame = waiting_for_first_frame_, + task_runners_.GetRasterTaskRunner()->PostTask( + [&waiting_for_first_frame = waiting_for_first_frame_, &waiting_for_first_frame_condition = waiting_for_first_frame_condition_, rasterizer = rasterizer_->GetWeakPtr(), pipeline = std::move(pipeline)]() { @@ -951,7 +979,7 @@ void Shell::OnAnimatorDraw(fml::RefPtr> pipeline) { void Shell::OnAnimatorDrawLastLayerTree() { FML_DCHECK(is_setup_); - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr()]() { if (rasterizer) { rasterizer->DrawLastLayerTree(); @@ -1008,7 +1036,7 @@ void Shell::HandleEngineSkiaMessage(fml::RefPtr message) { if (args == root.MemberEnd() || !args->value.IsInt()) return; - task_runners_.GetGPUTaskRunner()->PostTask( + task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), max_bytes = args->value.GetInt(), response = std::move(message->response())] { if (rasterizer) { @@ -1057,7 +1085,7 @@ void Shell::SetNeedsReportTimings(bool value) { void Shell::ReportTimings() { FML_DCHECK(is_setup_); - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto timings = std::move(unreported_timings_); unreported_timings_ = {}; @@ -1069,15 +1097,15 @@ void Shell::ReportTimings() { } size_t Shell::UnreportedFramesCount() const { - // Check that this is running on the GPU thread to avoid race conditions. - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + // Check that this is running on the raster thread to avoid race conditions. + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); FML_DCHECK(unreported_timings_.size() % FrameTiming::kCount == 0); return unreported_timings_.size() / FrameTiming::kCount; } void Shell::OnFrameRasterized(const FrameTiming& timing) { FML_DCHECK(is_setup_); - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); // The C++ callback defined in settings.h and set by Flutter runner. This is // independent of the timings report to the Dart side. @@ -1120,7 +1148,7 @@ void Shell::OnFrameRasterized(const FrameTiming& timing) { // second. Otherwise, the timings of last few frames of an animation may // never be reported until the next animation starts. frame_timings_report_scheduled_ = true; - task_runners_.GetGPUTaskRunner()->PostDelayedTask( + task_runners_.GetRasterTaskRunner()->PostDelayedTask( [self = weak_factory_gpu_->GetWeakPtr()]() { if (!self.get()) { return; @@ -1142,6 +1170,13 @@ fml::Milliseconds Shell::GetFrameBudget() { } } +fml::TimePoint Shell::GetLatestFrameTargetTime() const { + std::scoped_lock time_recorder_lock(time_recorder_mutex_); + FML_CHECK(latest_frame_target_time_.has_value()) + << "GetLatestFrameTargetTime called before OnAnimatorBeginFrame"; + return latest_frame_target_time_.value(); +} + // |ServiceProtocol::Handler| fml::RefPtr Shell::GetServiceProtocolHandlerTaskRunner( std::string_view method) const { @@ -1201,7 +1236,7 @@ static void ServiceProtocolFailureError(rapidjson::Document& response, bool Shell::OnServiceProtocolScreenshot( const ServiceProtocol::Handler::ServiceProtocolMap& params, rapidjson::Document& response) { - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::CompressedImage, true); if (screenshot.data) { @@ -1222,7 +1257,7 @@ bool Shell::OnServiceProtocolScreenshot( bool Shell::OnServiceProtocolScreenshotSKP( const ServiceProtocol::Handler::ServiceProtocolMap& params, rapidjson::Document& response) { - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::SkiaPicture, true); if (screenshot.data) { @@ -1251,16 +1286,6 @@ bool Shell::OnServiceProtocolRunInView( return false; } - // TODO(chinmaygarde): In case of hot-reload from .dill files, the packages - // file is ignored. Currently, the tool is passing a junk packages file to - // pass this check. Update the service protocol interface and remove this - // workaround. - if (params.count("packagesFile") == 0) { - ServiceProtocolParameterError(response, - "'packagesFile' parameter is missing."); - return false; - } - if (params.count("assetDirectory") == 0) { ServiceProtocolParameterError(response, "'assetDirectory' parameter is missing."); @@ -1269,8 +1294,6 @@ bool Shell::OnServiceProtocolRunInView( std::string main_script_path = fml::paths::FromURI(params.at("mainScript").data()); - std::string packages_path = - fml::paths::FromURI(params.at("packagesFile").data()); std::string asset_directory_path = fml::paths::FromURI(params.at("assetDirectory").data()); @@ -1289,6 +1312,7 @@ bool Shell::OnServiceProtocolRunInView( configuration.AddAssetResolver( std::make_unique(fml::OpenDirectory( asset_directory_path.c_str(), false, fml::FilePermission::kRead))); + PersistentCache::UpdateAssetPath(asset_directory_path); auto& allocator = response.GetAllocator(); response.SetObject(); @@ -1337,6 +1361,32 @@ bool Shell::OnServiceProtocolGetDisplayRefreshRate( return true; } +bool Shell::OnServiceProtocolGetSkSLs( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document& response) { + FML_DCHECK(task_runners_.GetIOTaskRunner()->RunsTasksOnCurrentThread()); + response.SetObject(); + response.AddMember("type", "GetSkSLs", response.GetAllocator()); + + rapidjson::Value shaders_json(rapidjson::kObjectType); + PersistentCache* persistent_cache = PersistentCache::GetCacheForProcess(); + std::vector sksls = persistent_cache->LoadSkSLs(); + for (const auto& sksl : sksls) { + size_t b64_size = + SkBase64::Encode(sksl.second->data(), sksl.second->size(), nullptr); + sk_sp b64_data = SkData::MakeUninitialized(b64_size + 1); + char* b64_char = static_cast(b64_data->writable_data()); + SkBase64::Encode(sksl.second->data(), sksl.second->size(), b64_char); + b64_char[b64_size] = 0; // make it null terminated for printing + rapidjson::Value shader_value(b64_char, response.GetAllocator()); + rapidjson::Value shader_key(PersistentCache::SkKeyToFilePath(*sksl.first), + response.GetAllocator()); + shaders_json.AddMember(shader_key, shader_value, response.GetAllocator()); + } + response.AddMember("SkSLs", shaders_json, response.GetAllocator()); + return true; +} + // Service protocol handler bool Shell::OnServiceProtocolSetAssetBundlePath( const ServiceProtocol::Handler::ServiceProtocolMap& params, @@ -1357,6 +1407,7 @@ bool Shell::OnServiceProtocolSetAssetBundlePath( asset_manager->PushFront(std::make_unique( fml::OpenDirectory(params.at("assetDirectory").data(), false, fml::FilePermission::kRead))); + PersistentCache::UpdateAssetPath(params.at("assetDirectory").data()); if (engine_->UpdateAssetManager(std::move(asset_manager))) { response.AddMember("type", "Success", allocator); @@ -1382,11 +1433,11 @@ Rasterizer::Screenshot Shell::Screenshot( fml::AutoResetWaitableEvent latch; Rasterizer::Screenshot screenshot; fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), [&latch, // - rasterizer = GetRasterizer(), // - &screenshot, // - screenshot_type, // - base64_encode // + task_runners_.GetRasterTaskRunner(), [&latch, // + rasterizer = GetRasterizer(), // + &screenshot, // + screenshot_type, // + base64_encode // ]() { if (rasterizer) { screenshot = rasterizer->ScreenshotLastLayerTree(screenshot_type, @@ -1401,7 +1452,7 @@ Rasterizer::Screenshot Shell::Screenshot( fml::Status Shell::WaitForFirstFrame(fml::TimeDelta timeout) { FML_DCHECK(is_setup_); if (task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread() || - task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()) { + task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()) { return fml::Status(fml::StatusCode::kFailedPrecondition, "WaitForFirstFrame called from thread that can't wait " "because it is responsible for generating the frame."); @@ -1410,7 +1461,7 @@ fml::Status Shell::WaitForFirstFrame(fml::TimeDelta timeout) { std::unique_lock lock(waiting_for_first_frame_mutex_); bool success = waiting_for_first_frame_condition_.wait_for( lock, std::chrono::milliseconds(timeout.ToMilliseconds()), - [& waiting_for_first_frame = waiting_for_first_frame_] { + [&waiting_for_first_frame = waiting_for_first_frame_] { return !waiting_for_first_frame.load(); }); if (success) { diff --git a/shell/common/shell.h b/shell/common/shell.h index d768c0673b973..b5245527ff6c6 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -6,6 +6,7 @@ #define SHELL_COMMON_SHELL_H_ #include +#include #include #include @@ -21,6 +22,7 @@ #include "flutter/fml/synchronization/sync_switch.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/fml/thread.h" +#include "flutter/fml/time/time_point.h" #include "flutter/lib/ui/semantics/custom_accessibility_action.h" #include "flutter/lib/ui/semantics/semantics_node.h" #include "flutter/lib/ui/window/platform_message.h" @@ -275,7 +277,7 @@ class Shell final : public PlatformView::Delegate, // Embedders should call this under low memory conditions to free up // internal caches used. // - // This method posts a task to the GPU threads to signal the Rasterizer to + // This method posts a task to the raster threads to signal the Rasterizer to // free resources. //---------------------------------------------------------------------------- @@ -368,6 +370,8 @@ class Shell final : public PlatformView::Delegate, const TaskRunners task_runners_; const Settings settings_; DartVMRef vm_; + mutable std::mutex time_recorder_mutex_; + std::optional latest_frame_target_time_; std::unique_ptr platform_view_; // on platform task runner std::unique_ptr engine_; // on UI task runner std::unique_ptr rasterizer_; // on GPU task runner @@ -393,7 +397,7 @@ class Shell final : public PlatformView::Delegate, std::mutex waiting_for_first_frame_mutex_; std::condition_variable waiting_for_first_frame_condition_; - // Written in the UI thread and read from the GPU thread. Hence make it + // Written in the UI thread and read from the raster thread. Hence make it // atomic. std::atomic needs_report_timings_{false}; @@ -407,10 +411,10 @@ class Shell final : public PlatformView::Delegate, std::vector unreported_timings_; // A cache of `Engine::GetDisplayRefreshRate` (only callable in the UI thread) - // so we can access it from `Rasterizer` (in the GPU thread). + // so we can access it from `Rasterizer` (in the raster thread). // // The atomic is for extra thread safety as this is written in the UI thread - // and read from the GPU thread. + // and read from the raster thread. std::atomic display_refresh_rate_ = 0.0f; // How many frames have been timed since last report. @@ -478,7 +482,7 @@ class Shell final : public PlatformView::Delegate, void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override; // |Animator::Delegate| - void OnAnimatorBeginFrame(fml::TimePoint frame_time) override; + void OnAnimatorBeginFrame(fml::TimePoint frame_target_time) override; // |Animator::Delegate| void OnAnimatorNotifyIdle(int64_t deadline) override; @@ -517,6 +521,9 @@ class Shell final : public PlatformView::Delegate, // |Rasterizer::Delegate| fml::Milliseconds GetFrameBudget() override; + // |Rasterizer::Delegate| + fml::TimePoint GetLatestFrameTargetTime() const override; + // |ServiceProtocol::Handler| fml::RefPtr GetServiceProtocolHandlerTaskRunner( std::string_view method) const override; @@ -561,9 +568,16 @@ class Shell final : public PlatformView::Delegate, const ServiceProtocol::Handler::ServiceProtocolMap& params, rapidjson::Document& response); + // Service protocol handler + // + // The returned SkSLs are base64 encoded. Decode before storing them to files. + bool OnServiceProtocolGetSkSLs( + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document& response); + fml::WeakPtrFactory weak_factory_; - // For accessing the Shell via the GPU thread, necessary for various + // For accessing the Shell via the raster thread, necessary for various // rasterizer callbacks. std::unique_ptr> weak_factory_gpu_; diff --git a/shell/common/shell_benchmarks.cc b/shell/common/shell_benchmarks.cc index 99d38e70d8143..c5b2751204ac4 100644 --- a/shell/common/shell_benchmarks.cc +++ b/shell/common/shell_benchmarks.cc @@ -48,7 +48,7 @@ static void StartupAndShutdownShell(benchmark::State& state, TaskRunners task_runners("test", thread_host->platform_thread->GetTaskRunner(), - thread_host->gpu_thread->GetTaskRunner(), + thread_host->raster_thread->GetTaskRunner(), thread_host->ui_thread->GetTaskRunner(), thread_host->io_thread->GetTaskRunner()); diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index 1267876fed44f..8ef045cf0a045 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -202,6 +202,31 @@ bool ShellTest::GetNeedsReportTimings(Shell* shell) { return shell->needs_report_timings_; } +void ShellTest::OnServiceProtocol( + Shell* shell, + ServiceProtocolEnum some_protocol, + fml::RefPtr task_runner, + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document& response) { + std::promise finished; + fml::TaskRunner::RunNowOrPostTask( + task_runner, [shell, some_protocol, params, &response, &finished]() { + switch (some_protocol) { + case ServiceProtocolEnum::kGetSkSLs: + shell->OnServiceProtocolGetSkSLs(params, response); + break; + case ServiceProtocolEnum::kSetAssetBundlePath: + shell->OnServiceProtocolSetAssetBundlePath(params, response); + break; + case ServiceProtocolEnum::kRunInView: + shell->OnServiceProtocolRunInView(params, response); + break; + } + finished.set_value(true); + }); + finished.get_future().wait(); +} + std::shared_ptr ShellTest::GetFontCollection( Shell* shell) { return shell->weak_engine_->GetFontCollection().GetFontCollection(); @@ -230,21 +255,28 @@ TaskRunners ShellTest::GetTaskRunnersForFixture() { return { "test", thread_host_.platform_thread->GetTaskRunner(), // platform - thread_host_.gpu_thread->GetTaskRunner(), // gpu + thread_host_.raster_thread->GetTaskRunner(), // raster thread_host_.ui_thread->GetTaskRunner(), // ui thread_host_.io_thread->GetTaskRunner() // io }; } +fml::TimePoint ShellTest::GetLatestFrameTargetTime(Shell* shell) const { + return shell->GetLatestFrameTargetTime(); +} + std::unique_ptr ShellTest::CreateShell(Settings settings, bool simulate_vsync) { return CreateShell(std::move(settings), GetTaskRunnersForFixture(), simulate_vsync); } -std::unique_ptr ShellTest::CreateShell(Settings settings, - TaskRunners task_runners, - bool simulate_vsync) { +std::unique_ptr ShellTest::CreateShell( + Settings settings, + TaskRunners task_runners, + bool simulate_vsync, + std::shared_ptr + shell_test_external_view_embedder) { const auto vsync_clock = std::make_shared(); CreateVsyncWaiter create_vsync_waiter = [&]() { if (simulate_vsync) { @@ -257,17 +289,18 @@ std::unique_ptr ShellTest::CreateShell(Settings settings, }; return Shell::Create( task_runners, settings, - [vsync_clock, &create_vsync_waiter](Shell& shell) { + [vsync_clock, &create_vsync_waiter, + shell_test_external_view_embedder](Shell& shell) { return ShellTestPlatformView::Create( shell, shell.GetTaskRunners(), vsync_clock, std::move(create_vsync_waiter), - ShellTestPlatformView::BackendType::kDefaultBackend); + ShellTestPlatformView::BackendType::kDefaultBackend, + shell_test_external_view_embedder); }, [](Shell& shell) { return std::make_unique(shell, shell.GetTaskRunners()); }); } - void ShellTest::DestroyShell(std::unique_ptr shell) { DestroyShell(std::move(shell), GetTaskRunnersForFixture()); } diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 9bd9f7fd39607..caf055120f0a0 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -11,9 +11,12 @@ #include "flutter/flow/layers/container_layer.h" #include "flutter/fml/build_config.h" #include "flutter/fml/macros.h" +#include "flutter/fml/time/time_point.h" #include "flutter/lib/ui/window/platform_message.h" +#include "flutter/shell/common/persistent_cache.h" #include "flutter/shell/common/run_configuration.h" #include "flutter/shell/common/shell.h" +#include "flutter/shell/common/shell_test_external_view_embedder.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/common/vsync_waiters_test.h" #include "flutter/testing/elf_loader.h" @@ -30,13 +33,18 @@ class ShellTest : public ThreadTest { Settings CreateSettingsForFixture(); std::unique_ptr CreateShell(Settings settings, bool simulate_vsync = false); - std::unique_ptr CreateShell(Settings settings, - TaskRunners task_runners, - bool simulate_vsync = false); + std::unique_ptr CreateShell( + Settings settings, + TaskRunners task_runners, + bool simulate_vsync = false, + std::shared_ptr + shell_test_external_view_embedder = nullptr); void DestroyShell(std::unique_ptr shell); void DestroyShell(std::unique_ptr shell, TaskRunners task_runners); TaskRunners GetTaskRunnersForFixture(); + fml::TimePoint GetLatestFrameTargetTime(Shell* shell) const; + void SendEnginePlatformMessage(Shell* shell, fml::RefPtr message); @@ -72,6 +80,22 @@ class ShellTest : public ThreadTest { static bool GetNeedsReportTimings(Shell* shell); static void SetNeedsReportTimings(Shell* shell, bool value); + enum ServiceProtocolEnum { + kGetSkSLs, + kSetAssetBundlePath, + kRunInView, + }; + + // Helper method to test private method Shell::OnServiceProtocolGetSkSLs. + // (ShellTest is a friend class of Shell.) We'll also make sure that it is + // running on the correct task_runner. + static void OnServiceProtocol( + Shell* shell, + ServiceProtocolEnum some_protocol, + fml::RefPtr task_runner, + const ServiceProtocol::Handler::ServiceProtocolMap& params, + rapidjson::Document& response); + std::shared_ptr GetFontCollection(Shell* shell); // Do not assert |UnreportedTimingsCount| to be positive in any tests. diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc new file mode 100644 index 0000000000000..683e21ad73c91 --- /dev/null +++ b/shell/common/shell_test_external_view_embedder.cc @@ -0,0 +1,55 @@ +#include "shell_test_external_view_embedder.h" + +namespace flutter { + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::CancelFrame() {} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::BeginFrame(SkISize frame_size, + GrContext* context, + double device_pixel_ratio) {} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) {} + +// |ExternalViewEmbedder| +PostPrerollResult ShellTestExternalViewEmbedder::PostPrerollAction( + fml::RefPtr raster_thread_merger) { + FML_DCHECK(raster_thread_merger); + return post_preroll_result_; +} + +// |ExternalViewEmbedder| +std::vector ShellTestExternalViewEmbedder::GetCurrentCanvases() { + return {}; +} + +// |ExternalViewEmbedder| +SkCanvas* ShellTestExternalViewEmbedder::CompositeEmbeddedView(int view_id) { + return nullptr; +} + +// |ExternalViewEmbedder| +bool ShellTestExternalViewEmbedder::SubmitFrame(GrContext* context, + SkCanvas* background_canvas) { + return true; +} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::FinishFrame() {} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::EndFrame( + fml::RefPtr raster_thread_merger) { + end_frame_call_back_(); +} + +// |ExternalViewEmbedder| +SkCanvas* ShellTestExternalViewEmbedder::GetRootCanvas() { + return nullptr; +} + +} // namespace flutter diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h new file mode 100644 index 0000000000000..56d9123ea8a7a --- /dev/null +++ b/shell/common/shell_test_external_view_embedder.h @@ -0,0 +1,72 @@ +// 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_SHELL_TEST_EXTERNAL_VIEW_EMBEDDER_H_ +#define FLUTTER_SHELL_TEST_EXTERNAL_VIEW_EMBEDDER_H_ + +#include "flutter/flow/embedded_views.h" +#include "flutter/fml/raster_thread_merger.h" + +namespace flutter { + +//------------------------------------------------------------------------------ +/// @brief The external view embedder used by |ShellTestPlatformViewGL| +/// +class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { + public: + using EndFrameCallBack = std::function; + + ShellTestExternalViewEmbedder(const EndFrameCallBack& end_frame_call_back, + PostPrerollResult post_preroll_result) + : end_frame_call_back_(end_frame_call_back), + post_preroll_result_(post_preroll_result) {} + + ~ShellTestExternalViewEmbedder() = default; + + private: + // |ExternalViewEmbedder| + void CancelFrame() override; + + // |ExternalViewEmbedder| + void BeginFrame(SkISize frame_size, + GrContext* context, + double device_pixel_ratio) override; + + // |ExternalViewEmbedder| + void PrerollCompositeEmbeddedView( + int view_id, + std::unique_ptr params) override; + + // |ExternalViewEmbedder| + PostPrerollResult PostPrerollAction( + fml::RefPtr raster_thread_merger) override; + + // |ExternalViewEmbedder| + std::vector GetCurrentCanvases() override; + + // |ExternalViewEmbedder| + SkCanvas* CompositeEmbeddedView(int view_id) override; + + // |ExternalViewEmbedder| + bool SubmitFrame(GrContext* context, SkCanvas* background_canvas) override; + + // |ExternalViewEmbedder| + void FinishFrame() override; + + // |ExternalViewEmbedder| + void EndFrame( + fml::RefPtr raster_thread_merger) override; + + // |ExternalViewEmbedder| + SkCanvas* GetRootCanvas() override; + + const EndFrameCallBack end_frame_call_back_; + const PostPrerollResult post_preroll_result_; + + FML_DISALLOW_COPY_AND_ASSIGN(ShellTestExternalViewEmbedder); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_TEST_EXTERNAL_VIEW_EMBEDDER_H_ diff --git a/shell/common/shell_test_platform_view.cc b/shell/common/shell_test_platform_view.cc index cfd43308bfc10..29bd24d3134ff 100644 --- a/shell/common/shell_test_platform_view.cc +++ b/shell/common/shell_test_platform_view.cc @@ -19,7 +19,9 @@ std::unique_ptr ShellTestPlatformView::Create( TaskRunners task_runners, std::shared_ptr vsync_clock, CreateVsyncWaiter create_vsync_waiter, - BackendType backend) { + BackendType backend, + std::shared_ptr + shell_test_external_view_embedder) { // TODO(gw280): https://github.com/flutter/flutter/issues/50298 // Make this fully runtime configurable switch (backend) { @@ -27,12 +29,14 @@ std::unique_ptr ShellTestPlatformView::Create( #ifdef SHELL_ENABLE_GL case BackendType::kGLBackend: return std::make_unique( - delegate, task_runners, vsync_clock, create_vsync_waiter); + delegate, task_runners, vsync_clock, create_vsync_waiter, + shell_test_external_view_embedder); #endif // SHELL_ENABLE_GL #ifdef SHELL_ENABLE_VULKAN case BackendType::kVulkanBackend: return std::make_unique( - delegate, task_runners, vsync_clock, create_vsync_waiter); + delegate, task_runners, vsync_clock, create_vsync_waiter, + shell_test_external_view_embedder); #endif // SHELL_ENABLE_VULKAN default: FML_LOG(FATAL) << "No backends supported for ShellTestPlatformView"; diff --git a/shell/common/shell_test_platform_view.h b/shell/common/shell_test_platform_view.h index c7aefe86b33bc..9a850b1d481d6 100644 --- a/shell/common/shell_test_platform_view.h +++ b/shell/common/shell_test_platform_view.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_COMMON_SHELL_TEST_PLATFORM_VIEW_H_ #include "flutter/shell/common/platform_view.h" +#include "flutter/shell/common/shell_test_external_view_embedder.h" #include "flutter/shell/common/vsync_waiters_test.h" namespace flutter { @@ -24,7 +25,9 @@ class ShellTestPlatformView : public PlatformView { TaskRunners task_runners, std::shared_ptr vsync_clock, CreateVsyncWaiter create_vsync_waiter, - BackendType backend); + BackendType backend, + std::shared_ptr + shell_test_external_view_embedder); virtual void SimulateVSync() = 0; diff --git a/shell/common/shell_test_platform_view_gl.cc b/shell/common/shell_test_platform_view_gl.cc index 2bd597575e523..c980c53168645 100644 --- a/shell/common/shell_test_platform_view_gl.cc +++ b/shell/common/shell_test_platform_view_gl.cc @@ -12,11 +12,14 @@ ShellTestPlatformViewGL::ShellTestPlatformViewGL( PlatformView::Delegate& delegate, TaskRunners task_runners, std::shared_ptr vsync_clock, - CreateVsyncWaiter create_vsync_waiter) + CreateVsyncWaiter create_vsync_waiter, + std::shared_ptr + shell_test_external_view_embedder) : ShellTestPlatformView(delegate, std::move(task_runners)), gl_surface_(SkISize::Make(800, 600)), create_vsync_waiter_(std::move(create_vsync_waiter)), - vsync_clock_(vsync_clock) {} + vsync_clock_(vsync_clock), + shell_test_external_view_embedder_(shell_test_external_view_embedder) {} ShellTestPlatformViewGL::~ShellTestPlatformViewGL() = default; @@ -70,7 +73,7 @@ ShellTestPlatformViewGL::GetGLProcResolver() const { // |GPUSurfaceGLDelegate| ExternalViewEmbedder* ShellTestPlatformViewGL::GetExternalViewEmbedder() { - return nullptr; + return shell_test_external_view_embedder_.get(); } } // namespace testing diff --git a/shell/common/shell_test_platform_view_gl.h b/shell/common/shell_test_platform_view_gl.h index 03db7910873e0..cac2fdc649781 100644 --- a/shell/common/shell_test_platform_view_gl.h +++ b/shell/common/shell_test_platform_view_gl.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_COMMON_SHELL_TEST_PLATFORM_VIEW_GL_H_ #define FLUTTER_SHELL_COMMON_SHELL_TEST_PLATFORM_VIEW_GL_H_ +#include "flutter/shell/common/shell_test_external_view_embedder.h" #include "flutter/shell/common/shell_test_platform_view.h" #include "flutter/shell/gpu/gpu_surface_gl_delegate.h" #include "flutter/testing/test_gl_surface.h" @@ -18,7 +19,9 @@ class ShellTestPlatformViewGL : public ShellTestPlatformView, ShellTestPlatformViewGL(PlatformView::Delegate& delegate, TaskRunners task_runners, std::shared_ptr vsync_clock, - CreateVsyncWaiter create_vsync_waiter); + CreateVsyncWaiter create_vsync_waiter, + std::shared_ptr + shell_test_external_view_embedder); virtual ~ShellTestPlatformViewGL() override; @@ -31,6 +34,9 @@ class ShellTestPlatformViewGL : public ShellTestPlatformView, std::shared_ptr vsync_clock_; + std::shared_ptr + shell_test_external_view_embedder_; + // |PlatformView| std::unique_ptr CreateRenderingSurface() override; diff --git a/shell/common/shell_test_platform_view_vulkan.cc b/shell/common/shell_test_platform_view_vulkan.cc index e9f41484d9881..3ed4193aa2584 100644 --- a/shell/common/shell_test_platform_view_vulkan.cc +++ b/shell/common/shell_test_platform_view_vulkan.cc @@ -11,11 +11,14 @@ ShellTestPlatformViewVulkan::ShellTestPlatformViewVulkan( PlatformView::Delegate& delegate, TaskRunners task_runners, std::shared_ptr vsync_clock, - CreateVsyncWaiter create_vsync_waiter) + CreateVsyncWaiter create_vsync_waiter, + std::shared_ptr + shell_test_external_view_embedder) : ShellTestPlatformView(delegate, std::move(task_runners)), create_vsync_waiter_(std::move(create_vsync_waiter)), vsync_clock_(vsync_clock), - proc_table_(fml::MakeRefCounted()) {} + proc_table_(fml::MakeRefCounted()), + shell_test_external_view_embedder_(shell_test_external_view_embedder) {} ShellTestPlatformViewVulkan::~ShellTestPlatformViewVulkan() = default; @@ -29,7 +32,8 @@ void ShellTestPlatformViewVulkan::SimulateVSync() { // |PlatformView| std::unique_ptr ShellTestPlatformViewVulkan::CreateRenderingSurface() { - return std::make_unique(proc_table_); + return std::make_unique(proc_table_, + shell_test_external_view_embedder_); } // |PlatformView| @@ -44,15 +48,21 @@ PointerDataDispatcherMaker ShellTestPlatformViewVulkan::GetDispatcherMaker() { // We need to merge this functionality back into //vulkan. // https://github.com/flutter/flutter/issues/51132 ShellTestPlatformViewVulkan::OffScreenSurface::OffScreenSurface( - fml::RefPtr vk) - : valid_(false), vk_(std::move(vk)) { + fml::RefPtr vk, + std::shared_ptr + shell_test_external_view_embedder) + : valid_(false), + vk_(std::move(vk)), + shell_test_external_view_embedder_(shell_test_external_view_embedder) { if (!vk_ || !vk_->HasAcquiredMandatoryProcAddresses()) { FML_DLOG(ERROR) << "Proc table has not acquired mandatory proc addresses."; return; } // Create the application instance. - std::vector extensions = {}; + std::vector extensions = { + VK_KHR_EXTERNAL_SEMAPHORE_CAPABILITIES_EXTENSION_NAME, + }; application_ = std::make_unique( *vk_, "FlutterTest", std::move(extensions)); @@ -170,5 +180,10 @@ SkMatrix ShellTestPlatformViewVulkan::OffScreenSurface::GetRootTransformation() return matrix; } +flutter::ExternalViewEmbedder* +ShellTestPlatformViewVulkan::OffScreenSurface::GetExternalViewEmbedder() { + return shell_test_external_view_embedder_.get(); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/shell_test_platform_view_vulkan.h b/shell/common/shell_test_platform_view_vulkan.h index 38bce87fa5414..31757f39a30b0 100644 --- a/shell/common/shell_test_platform_view_vulkan.h +++ b/shell/common/shell_test_platform_view_vulkan.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_COMMON_SHELL_TEST_PLATFORM_VIEW_VULKAN_H_ #define FLUTTER_SHELL_COMMON_SHELL_TEST_PLATFORM_VIEW_VULKAN_H_ +#include "flutter/shell/common/shell_test_external_view_embedder.h" #include "flutter/shell/common/shell_test_platform_view.h" #include "flutter/shell/gpu/gpu_surface_vulkan_delegate.h" #include "flutter/vulkan/vulkan_application.h" @@ -18,7 +19,9 @@ class ShellTestPlatformViewVulkan : public ShellTestPlatformView { ShellTestPlatformViewVulkan(PlatformView::Delegate& delegate, TaskRunners task_runners, std::shared_ptr vsync_clock, - CreateVsyncWaiter create_vsync_waiter); + CreateVsyncWaiter create_vsync_waiter, + std::shared_ptr + shell_test_external_view_embedder); ~ShellTestPlatformViewVulkan() override; @@ -27,7 +30,9 @@ class ShellTestPlatformViewVulkan : public ShellTestPlatformView { private: class OffScreenSurface : public flutter::Surface { public: - OffScreenSurface(fml::RefPtr vk); + OffScreenSurface(fml::RefPtr vk, + std::shared_ptr + shell_test_external_view_embedder); ~OffScreenSurface() override; @@ -42,9 +47,13 @@ class ShellTestPlatformViewVulkan : public ShellTestPlatformView { // |Surface| GrContext* GetContext() override; + flutter::ExternalViewEmbedder* GetExternalViewEmbedder() override; + private: bool valid_; fml::RefPtr vk_; + std::shared_ptr + shell_test_external_view_embedder_; std::unique_ptr application_; std::unique_ptr logical_device_; sk_sp context_; @@ -61,6 +70,9 @@ class ShellTestPlatformViewVulkan : public ShellTestPlatformView { fml::RefPtr proc_table_; + std::shared_ptr + shell_test_external_view_embedder_; + // |PlatformView| std::unique_ptr CreateRenderingSurface() override; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 26963e1c146be..e96ab7c441b99 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -19,14 +19,18 @@ #include "flutter/fml/synchronization/count_down_latch.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/persistent_cache.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/common/rasterizer.h" #include "flutter/shell/common/shell_test.h" +#include "flutter/shell/common/shell_test_external_view_embedder.h" #include "flutter/shell/common/shell_test_platform_view.h" #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/common/vsync_waiter_fallback.h" +#include "flutter/shell/version/version.h" #include "flutter/testing/testing.h" +#include "rapidjson/writer.h" #include "third_party/tonic/converter/dart_converter.h" namespace flutter { @@ -72,7 +76,7 @@ TEST_F(ShellTest, InitializeWithDifferentThreads) { ThreadHost::Type::Platform | ThreadHost::Type::GPU | ThreadHost::Type::IO | ThreadHost::Type::UI); TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(), - thread_host.gpu_thread->GetTaskRunner(), + thread_host.raster_thread->GetTaskRunner(), thread_host.ui_thread->GetTaskRunner(), thread_host.io_thread->GetTaskRunner()); auto shell = CreateShell(std::move(settings), std::move(task_runners)); @@ -121,7 +125,7 @@ TEST_F(ShellTest, fml::MessageLoop::EnsureInitializedForCurrentThread(); TaskRunners task_runners("test", fml::MessageLoop::GetCurrent().GetTaskRunner(), - thread_host.gpu_thread->GetTaskRunner(), + thread_host.raster_thread->GetTaskRunner(), thread_host.ui_thread->GetTaskRunner(), thread_host.io_thread->GetTaskRunner()); auto shell = Shell::Create( @@ -136,7 +140,7 @@ TEST_F(ShellTest, return static_cast>( std::make_unique(task_runners)); }, - ShellTestPlatformView::BackendType::kDefaultBackend); + ShellTestPlatformView::BackendType::kDefaultBackend, nullptr); }, [](Shell& shell) { return std::make_unique(shell, shell.GetTaskRunners()); @@ -156,7 +160,7 @@ TEST_F(ShellTest, InitializeWithGPUAndPlatformThreadsTheSame) { TaskRunners task_runners( "test", thread_host.platform_thread->GetTaskRunner(), // platform - thread_host.platform_thread->GetTaskRunner(), // gpu + thread_host.platform_thread->GetTaskRunner(), // raster thread_host.ui_thread->GetTaskRunner(), // ui thread_host.io_thread->GetTaskRunner() // io ); @@ -267,21 +271,19 @@ TEST_F(ShellTest, BlacklistedDartVMFlag) { fml::CommandLine::Option("dart-flags", "--verify_after_gc")}; fml::CommandLine command_line("", options, std::vector()); -#if !FLUTTER_RELEASE // Upon encountering a non-whitelisted Dart flag the process terminates. const char* expected = "Encountered blacklisted Dart VM flag: --verify_after_gc"; ASSERT_DEATH(flutter::SettingsFromCommandLine(command_line), expected); -#else - flutter::Settings settings = flutter::SettingsFromCommandLine(command_line); - EXPECT_EQ(settings.dart_flags.size(), 0u); -#endif } TEST_F(ShellTest, WhitelistedDartVMFlag) { const std::vector options = { - fml::CommandLine::Option("dart-flags", - "--max_profile_depth 1,--random_seed 42")}; +#if !FLUTTER_RELEASE + fml::CommandLine::Option("dart-flags", + "--max_profile_depth 1,--random_seed 42") +#endif + }; fml::CommandLine command_line("", options, std::vector()); flutter::Settings settings = flutter::SettingsFromCommandLine(command_line); @@ -465,6 +467,51 @@ TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { DestroyShell(std::move(shell)); } +TEST_F(ShellTest, + ExternalEmbedderEndFrameIsCalledWhenPostPrerollResultIsResubmit) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent endFrameLatch; + bool end_frame_called = false; + auto end_frame_callback = [&] { + end_frame_called = true; + endFrameLatch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kResubmitFrame); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + SkPictureRecorder recorder; + SkCanvas* recording_canvas = + recorder.beginRecording(SkRect::MakeXYWH(0, 0, 80, 80)); + recording_canvas->drawRect(SkRect::MakeXYWH(0, 0, 80, 80), + SkPaint(SkColor4f::FromColor(SK_ColorRED))); + auto sk_picture = recorder.finishRecordingAsPicture(); + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(10, 10), + flutter::SkiaGPUObject({sk_picture, queue}), false, false); + root->Add(picture_layer); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + endFrameLatch.Wait(); + + ASSERT_TRUE(end_frame_called); + + DestroyShell(std::move(shell)); +} + TEST(SettingsTest, FrameTimingSetsAndGetsProperly) { // Ensure that all phases are in kPhases. ASSERT_EQ(sizeof(FrameTiming::kPhases), @@ -714,7 +761,7 @@ static size_t GetRasterizerResourceCacheBytesSync(Shell& shell) { size_t bytes = 0; fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - shell.GetTaskRunners().GetGPUTaskRunner(), [&]() { + shell.GetTaskRunners().GetRasterTaskRunner(), [&]() { if (auto rasterizer = shell.GetRasterizer()) { bytes = rasterizer->GetResourceCacheMaxBytes().value_or(0U); } @@ -885,7 +932,7 @@ class MockTexture : public Texture { ~MockTexture() override = default; - // Called from GPU thread. + // Called from raster thread. void Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, @@ -935,7 +982,7 @@ TEST_F(ShellTest, TextureFrameMarkedAvailableAndUnregister) { std::make_shared(0, latch); fml::TaskRunner::RunNowOrPostTask( - shell->GetTaskRunners().GetGPUTaskRunner(), [&]() { + shell->GetTaskRunners().GetRasterTaskRunner(), [&]() { shell->GetPlatformView()->RegisterTexture(mockTexture); shell->GetPlatformView()->MarkTextureFrameAvailable(0); }); @@ -944,7 +991,7 @@ TEST_F(ShellTest, TextureFrameMarkedAvailableAndUnregister) { EXPECT_EQ(mockTexture->frames_available(), 1); fml::TaskRunner::RunNowOrPostTask( - shell->GetTaskRunners().GetGPUTaskRunner(), + shell->GetTaskRunners().GetRasterTaskRunner(), [&]() { shell->GetPlatformView()->UnregisterTexture(0); }); latch->Wait(); @@ -960,7 +1007,7 @@ TEST_F(ShellTest, IsolateCanAccessPersistentIsolateData) { std::make_shared(message); TaskRunners task_runners("test", // label GetCurrentTaskRunner(), // platform - CreateNewThread(), // gpu + CreateNewThread(), // raster CreateNewThread(), // ui CreateNewThread() // io ); @@ -1029,7 +1076,7 @@ TEST_F(ShellTest, Screenshot) { auto screenshot_future = screenshot_promise.get_future(); fml::TaskRunner::RunNowOrPostTask( - shell->GetTaskRunners().GetGPUTaskRunner(), + shell->GetTaskRunners().GetRasterTaskRunner(), [&screenshot_promise, &shell]() { auto rasterizer = shell->GetRasterizer(); screenshot_promise.set_value(rasterizer->ScreenshotLastLayerTree( @@ -1129,5 +1176,63 @@ TEST_F(ShellTest, CanDecompressImageFromAsset) { DestroyShell(std::move(shell)); } +TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) { + // Create 2 dummpy SkSL cache file IE (base32 encoding of A), II (base32 + // encoding of B) with content x and y. + fml::ScopedTemporaryDirectory temp_dir; + PersistentCache::SetCacheDirectoryPath(temp_dir.path()); + PersistentCache::ResetCacheForProcess(); + std::vector components = {"flutter_engine", + GetFlutterEngineVersion(), "skia", + GetSkiaVersion(), "sksl"}; + auto sksl_dir = fml::CreateDirectory(temp_dir.fd(), components, + fml::FilePermission::kReadWrite); + const std::string x = "x"; + const std::string y = "y"; + auto x_data = std::make_unique( + std::vector{x.begin(), x.end()}); + auto y_data = std::make_unique( + std::vector{y.begin(), y.end()}); + ASSERT_TRUE(fml::WriteAtomically(sksl_dir, "IE", *x_data)); + ASSERT_TRUE(fml::WriteAtomically(sksl_dir, "II", *y_data)); + + Settings settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + ServiceProtocol::Handler::ServiceProtocolMap empty_params; + rapidjson::Document document; + OnServiceProtocol(shell.get(), ServiceProtocolEnum::kGetSkSLs, + shell->GetTaskRunners().GetIOTaskRunner(), empty_params, + document); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + DestroyShell(std::move(shell)); + + const std::string expected_json1 = + "{\"type\":\"GetSkSLs\",\"SkSLs\":{\"II\":\"eQ==\",\"IE\":\"eA==\"}}"; + const std::string expected_json2 = + "{\"type\":\"GetSkSLs\",\"SkSLs\":{\"IE\":\"eA==\",\"II\":\"eQ==\"}}"; + bool json_is_expected = (expected_json1 == buffer.GetString()) || + (expected_json2 == buffer.GetString()); + ASSERT_TRUE(json_is_expected) << buffer.GetString() << " is not equal to " + << expected_json1 << " or " << expected_json2; + + // Cleanup files + fml::FileVisitor recursive_cleanup = [&recursive_cleanup]( + const fml::UniqueFD& directory, + const std::string& filename) { + if (fml::IsDirectory(directory, filename.c_str())) { + fml::UniqueFD sub_dir = + OpenDirectoryReadOnly(directory, filename.c_str()); + VisitFiles(sub_dir, recursive_cleanup); + fml::UnlinkDirectory(directory, filename.c_str()); + } else { + fml::UnlinkFile(directory, filename.c_str()); + } + return true; + }; + VisitFiles(temp_dir.fd(), recursive_cleanup); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/switches.cc b/shell/common/switches.cc index a23e1156f5e5e..0f0c7fc145a78 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -237,6 +237,9 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { } } + settings.disable_http = + command_line.HasOption(FlagForSwitch(Switch::DisableHttp)); + // Disable need for authentication codes for VM service communication, if // specified. settings.disable_service_auth_codes = @@ -272,6 +275,9 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { settings.trace_skia = command_line.HasOption(FlagForSwitch(Switch::TraceSkia)); + command_line.GetOptionValue(FlagForSwitch(Switch::TraceWhitelist), + &settings.trace_whitelist); + settings.trace_systrace = command_line.HasOption(FlagForSwitch(Switch::TraceSystrace)); diff --git a/shell/common/switches.h b/shell/common/switches.h index c0faac25d50c8..f261a76e141e1 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -95,13 +95,13 @@ DEF_SWITCH(EndlessTraceBuffer, "indefinitely however.") DEF_SWITCH(EnableSoftwareRendering, "enable-software-rendering", - "Enable rendering using the Skia software backend. This is useful" - "when testing Flutter on emulators. By default, Flutter will" + "Enable rendering using the Skia software backend. This is useful " + "when testing Flutter on emulators. By default, Flutter will " "attempt to either use OpenGL or Vulkan.") DEF_SWITCH(SkiaDeterministicRendering, "skia-deterministic-rendering", - "Skips the call to SkGraphics::Init(), thus avoiding swapping out" - "some Skia function pointers based on available CPU features. This" + "Skips the call to SkGraphics::Init(), thus avoiding swapping out " + "some Skia function pointers based on available CPU features. This " "is used to obtain 100% deterministic behavior in Skia rendering.") DEF_SWITCH(FlutterAssetsDir, "flutter-assets-dir", @@ -129,6 +129,11 @@ DEF_SWITCH(TraceSkia, "Trace Skia calls. This is useful when debugging the GPU threed." "By default, Skia tracing is not enabled to reduce the number of " "traced events") +DEF_SWITCH( + TraceWhitelist, + "trace-whitelist", + "Filters out all trace events except those that are specified in this " + "comma separated list of whitelisted prefixes.") DEF_SWITCH(DumpSkpOnShaderCompilation, "dump-skp-on-shader-compilation", "Automatically dump the skp that triggers new shader compilations. " @@ -169,6 +174,12 @@ DEF_SWITCH(DisableDartAsserts, "disabled. This flag may be specified if the user wishes to run " "with assertions disabled in the debug product mode (i.e. with JIT " "or DBC).") +DEF_SWITCH(DisableHttp, + "disable-http", + "Dart VM has a master switch that can be set to disable insecure " + "HTTP and WebSocket protocols. Localhost or loopback addresses are " + "exempted. This flag can be specified if the embedder wants this " + "for a particular platform.") DEF_SWITCH( ForceMultithreading, "force-multithreading", diff --git a/shell/common/thread_host.cc b/shell/common/thread_host.cc index 0ab677a76b7bd..5e2201e097a86 100644 --- a/shell/common/thread_host.cc +++ b/shell/common/thread_host.cc @@ -20,7 +20,7 @@ ThreadHost::ThreadHost(std::string name_prefix, uint64_t mask) { } if (mask & ThreadHost::Type::GPU) { - gpu_thread = std::make_unique(name_prefix + ".gpu"); + raster_thread = std::make_unique(name_prefix + ".raster"); } if (mask & ThreadHost::Type::IO) { @@ -33,7 +33,7 @@ ThreadHost::~ThreadHost() = default; void ThreadHost::Reset() { platform_thread.reset(); ui_thread.reset(); - gpu_thread.reset(); + raster_thread.reset(); io_thread.reset(); } diff --git a/shell/common/thread_host.h b/shell/common/thread_host.h index b5e14e6fa6bf8..9f2643ed7e4f0 100644 --- a/shell/common/thread_host.h +++ b/shell/common/thread_host.h @@ -23,7 +23,7 @@ struct ThreadHost { std::unique_ptr platform_thread; std::unique_ptr ui_thread; - std::unique_ptr gpu_thread; + std::unique_ptr raster_thread; std::unique_ptr io_thread; ThreadHost(); diff --git a/shell/gpu/gpu_surface_metal.mm b/shell/gpu/gpu_surface_metal.mm index 154e951319bdb..5ca8b6b4a42d5 100644 --- a/shell/gpu/gpu_surface_metal.mm +++ b/shell/gpu/gpu_surface_metal.mm @@ -39,32 +39,25 @@ } // |Surface| -std::unique_ptr GPUSurfaceMetal::AcquireFrame(const SkISize& size) { +std::unique_ptr GPUSurfaceMetal::AcquireFrame(const SkISize& frame_size) { if (!IsValid()) { FML_LOG(ERROR) << "Metal surface was invalid."; return nullptr; } - if (size.isEmpty()) { + if (frame_size.isEmpty()) { FML_LOG(ERROR) << "Metal surface was asked for an empty frame."; return nullptr; } - const auto bounds = layer_.get().bounds.size; - const auto scale = layer_.get().contentsScale; - if (bounds.width <= 0.0 || bounds.height <= 0.0 || scale <= 0.0) { - FML_LOG(ERROR) << "Metal layer bounds were invalid."; - return nullptr; - } + const auto drawable_size = CGSizeMake(frame_size.width(), frame_size.height()); - const auto scaled_bounds = CGSizeMake(bounds.width * scale, bounds.height * scale); + if (!CGSizeEqualToSize(drawable_size, layer_.get().drawableSize)) { + layer_.get().drawableSize = drawable_size; + } ReleaseUnusedDrawableIfNecessary(); - if (!CGSizeEqualToSize(scaled_bounds, layer_.get().drawableSize)) { - layer_.get().drawableSize = scaled_bounds; - } - auto surface = SkSurface::MakeFromCAMetalLayer(context_.get(), // context layer_.get(), // layer kTopLeft_GrSurfaceOrigin, // origin @@ -89,8 +82,6 @@ return false; } - const auto has_external_view_embedder = delegate_->GetExternalViewEmbedder() != nullptr; - auto command_buffer = fml::scoped_nsprotocol>([[command_queue_.get() commandBuffer] retain]); @@ -98,21 +89,10 @@ reinterpret_cast>(next_drawable_)); next_drawable_ = nullptr; - // External views need to present with transaction. When presenting with - // transaction, we have to block, otherwise we risk presenting the drawable - // after the CATransaction has completed. - // See: - // https://developer.apple.com/documentation/quartzcore/cametallayer/1478157-presentswithtransaction - // TODO(dnfield): only do this if transactions are actually being used. - // https://github.com/flutter/flutter/issues/24133 - if (!has_external_view_embedder) { - [command_buffer.get() presentDrawable:drawable.get()]; - [command_buffer.get() commit]; - } else { - [command_buffer.get() commit]; - [command_buffer.get() waitUntilScheduled]; - [drawable.get() present]; - } + [command_buffer.get() commit]; + [command_buffer.get() waitUntilScheduled]; + [drawable.get() present]; + return true; }; diff --git a/shell/gpu/gpu_surface_vulkan.cc b/shell/gpu/gpu_surface_vulkan.cc index 3d83eb8afca0e..22d67f7a777e2 100644 --- a/shell/gpu/gpu_surface_vulkan.cc +++ b/shell/gpu/gpu_surface_vulkan.cc @@ -42,9 +42,9 @@ std::unique_ptr GPUSurfaceVulkan::AcquireFrame( SurfaceFrame::SubmitCallback callback = [weak_this = weak_factory_.GetWeakPtr()](const SurfaceFrame&, SkCanvas* canvas) -> bool { - // Frames are only ever acquired on the GPU thread. This is also the thread - // on which the weak pointer factory is collected (as this instance is owned - // by the rasterizer). So this use of weak pointers is safe. + // Frames are only ever acquired on the raster thread. This is also the + // thread on which the weak pointer factory is collected (as this instance + // is owned by the rasterizer). So this use of weak pointers is safe. if (canvas == nullptr || !weak_this) { return false; } diff --git a/shell/platform/BUILD.gn b/shell/platform/BUILD.gn index 4b881909d14e2..925177e67472b 100644 --- a/shell/platform/BUILD.gn +++ b/shell/platform/BUILD.gn @@ -27,11 +27,9 @@ group("platform") { "windows", ] } else if (is_fuchsia) { - if (using_fuchsia_sdk) { - deps = [ - "fuchsia", - ] - } + deps = [ + "fuchsia", + ] } else { assert(false, "Unknown/Unsupported platform.") } diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 940801e7f3e03..57992c62961c2 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -255,15 +255,24 @@ action("flutter_shell_java") { source_jar_path, source_jar_path + ".md5.stamp", ] - inputs = [ android_sdk_jar ] + embedding_dependencies_jars + + lambda_jar = "$android_sdk_build_tools/core-lambda-stubs.jar" + inputs = [ + android_sdk_jar, + lambda_jar, + ] + embedding_dependencies_jars _rebased_current_path = rebase_path(".") _rebased_jar_path = rebase_path(jar_path, root_build_dir) _rebased_source_jar_path = rebase_path(source_jar_path, root_build_dir) _rebased_depfile = rebase_path(depfile, root_build_dir) _rebased_android_sdk_jar = rebase_path(android_sdk_jar, root_build_dir) - _rebased_classpath = [ _rebased_android_sdk_jar ] + - rebase_path(embedding_dependencies_jars, root_build_dir) + _rebased_lambda_jar = rebase_path(lambda_jar, root_build_dir) + _rebased_classpath = + [ + _rebased_android_sdk_jar, + _rebased_lambda_jar, + ] + rebase_path(embedding_dependencies_jars, root_build_dir) args = [ "--depfile=$_rebased_depfile", @@ -428,7 +437,6 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", - "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/external/FlutterLaunchTests.java", "test/io/flutter/plugin/common/StandardMessageCodecTest.java", "test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java", diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index cb54c2b736c48..fe4d85ab7b259 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -45,13 +45,13 @@ AndroidShellHolder::AndroidShellHolder( ThreadHost::Type::IO}; } - // Detach from JNI when the UI and GPU threads exit. + // Detach from JNI when the UI and raster threads exit. auto jni_exit_task([key = thread_destruct_key_]() { FML_CHECK(pthread_setspecific(key, reinterpret_cast(1)) == 0); }); thread_host_.ui_thread->GetTaskRunner()->PostTask(jni_exit_task); if (!is_background_view) { - thread_host_.gpu_thread->GetTaskRunner()->PostTask(jni_exit_task); + thread_host_.raster_thread->GetTaskRunner()->PostTask(jni_exit_task); } fml::WeakPtr weak_platform_view; @@ -96,13 +96,13 @@ AndroidShellHolder::AndroidShellHolder( ui_runner = single_task_runner; io_runner = single_task_runner; } else { - gpu_runner = thread_host_.gpu_thread->GetTaskRunner(); + gpu_runner = thread_host_.raster_thread->GetTaskRunner(); ui_runner = thread_host_.ui_thread->GetTaskRunner(); io_runner = thread_host_.io_thread->GetTaskRunner(); } flutter::TaskRunners task_runners(thread_label, // label platform_runner, // platform - gpu_runner, // gpu + gpu_runner, // raster ui_runner, // ui io_runner // io ); @@ -121,10 +121,10 @@ AndroidShellHolder::AndroidShellHolder( is_valid_ = shell_ != nullptr; if (is_valid_) { - task_runners.GetGPUTaskRunner()->PostTask([]() { + task_runners.GetRasterTaskRunner()->PostTask([]() { // Android describes -8 as "most important display threads, for // compositing the screen and retrieving input events". Conservatively - // set the GPU thread to slightly lower priority than it. + // set the raster thread to slightly lower priority than it. if (::setpriority(PRIO_PROCESS, gettid(), -5) != 0) { // Defensive fallback. Depending on the OEM, it may not be possible // to set priority to -5. diff --git a/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java index 2e881b6148673..3fc80cf703bb8 100644 --- a/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java @@ -125,9 +125,10 @@ protected void onPostResume() { eventDelegate.onPostResume(); } - // @Override - added in API level 23 + @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); eventDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); } diff --git a/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java b/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java index b1979694cffdf..2a7173634ec6b 100644 --- a/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java +++ b/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java @@ -86,7 +86,7 @@ public void attach(FlutterView flutterView, Activity activity) { public void detach() { mPlatformViewsController.detach(); - mPlatformViewsController.onFlutterViewDestroyed(); + mPlatformViewsController.onDetachedFromJNI(); mFlutterView = null; mActivity = null; } @@ -239,6 +239,6 @@ public boolean onViewDestroy(FlutterNativeView view) { } public void destroy() { - mPlatformViewsController.onFlutterViewDestroyed(); + mPlatformViewsController.onDetachedFromJNI(); } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 9a7b6aa7d0925..4215d4d2c39c4 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -489,6 +489,7 @@ public void onBackPressed() { @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); flutterFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 5d782b678ff6f..31cbeb9673484 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -264,7 +264,7 @@ private FlutterView( super(context, attrs); this.flutterTextureView = flutterTextureView; - this.renderSurface = flutterSurfaceView; + this.renderSurface = flutterTextureView; init(); } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index dfd2bd674c57c..271a1a8ec24dc 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -225,6 +225,7 @@ public FlutterEngine( textInputChannel = new TextInputChannel(dartExecutor); this.platformViewsController = platformViewsController; + this.platformViewsController.onAttachedToJNI(); this.pluginRegistry = new FlutterEnginePluginRegistry(context.getApplicationContext(), this, flutterLoader); @@ -289,6 +290,7 @@ public void destroy() { Log.v(TAG, "Destroying."); // The order that these things are destroyed is important. pluginRegistry.destroy(); + platformViewsController.onDetachedFromJNI(); dartExecutor.onDetachedFromJNI(); flutterJNI.removeEngineLifecycleListener(engineLifecycleListener); flutterJNI.detachFromNativeAndReleaseResources(); diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index 3c7d33b306765..f7eff27e60423 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -5,7 +5,6 @@ package io.flutter.embedding.engine.systemchannels; import android.content.pm.ActivityInfo; -import android.graphics.Rect; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -15,7 +14,6 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import org.json.JSONArray; import org.json.JSONException; @@ -125,31 +123,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result platformMessageHandler.popSystemNavigator(); result.success(null); break; - case "SystemGestures.getSystemGestureExclusionRects": - List exclusionRects = platformMessageHandler.getSystemGestureExclusionRects(); - if (exclusionRects == null) { - String incorrectApiLevel = "Exclusion rects only exist for Android API 29+."; - result.error("error", incorrectApiLevel, null); - break; - } - - ArrayList> encodedExclusionRects = - encodeExclusionRects(exclusionRects); - result.success(encodedExclusionRects); - break; - case "SystemGestures.setSystemGestureExclusionRects": - if (!(arguments instanceof JSONArray)) { - String inputTypeError = - "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; - result.error("inputTypeError", inputTypeError, null); - break; - } - - JSONArray inputRects = (JSONArray) arguments; - ArrayList decodedRects = decodeExclusionRects(inputRects); - platformMessageHandler.setSystemGestureExclusionRects(decodedRects); - result.success(null); - break; case "Clipboard.getData": { String contentFormatName = (String) arguments; @@ -295,68 +268,6 @@ private int decodeOrientations(@NonNull JSONArray encodedOrientations) return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } - /** - * Decodes a JSONArray of rectangle data into an ArrayList. - * - *

Since View.setSystemGestureExclusionRects receives a JSONArray containing JSONObjects, these - * values need to be transformed into the expected input of View.setSystemGestureExclusionRects, - * which is ArrayList. - * - *

This method is used by the SystemGestures.setSystemGestureExclusionRects platform channel. - * - * @throws JSONException if {@code inputRects} does not contain expected keys and value types. - */ - @NonNull - private ArrayList decodeExclusionRects(@NonNull JSONArray inputRects) throws JSONException { - ArrayList exclusionRects = new ArrayList(); - for (int i = 0; i < inputRects.length(); i++) { - JSONObject rect = inputRects.getJSONObject(i); - int top; - int right; - int bottom; - int left; - - try { - top = rect.getInt("top"); - right = rect.getInt("right"); - bottom = rect.getInt("bottom"); - left = rect.getInt("left"); - } catch (JSONException exception) { - throw new JSONException( - "Incorrect JSON data shape. To set system gesture exclusion rects, \n" - + "a JSONObject with top, right, bottom and left values need to be set to int values."); - } - - Rect gestureRect = new Rect(left, top, right, bottom); - exclusionRects.add(gestureRect); - } - - return exclusionRects; - } - - /** - * Encodes a List provided by the Android host into an ArrayList>. - * - *

Since View.getSystemGestureExclusionRects returns a list of Rects, these Rects need to be - * transformed into UTF-8 encoded JSON messages to be properly decoded by the Flutter framework. - * - *

This method is used by the SystemGestures.getSystemGestureExclusionRects platform channel. - */ - private ArrayList> encodeExclusionRects(List exclusionRects) { - ArrayList> encodedExclusionRects = - new ArrayList>(); - for (Rect rect : exclusionRects) { - HashMap rectMap = new HashMap(); - rectMap.put("top", rect.top); - rectMap.put("right", rect.right); - rectMap.put("bottom", rect.bottom); - rectMap.put("left", rect.left); - encodedExclusionRects.add(rectMap); - } - - return encodedExclusionRects; - } - @NonNull private AppSwitcherDescription decodeAppSwitcherDescription( @NonNull JSONObject encodedDescription) throws JSONException { @@ -515,15 +426,6 @@ public interface PlatformMessageHandler { * {@code text}. */ void setClipboardData(@NonNull String text); - - /** The Flutter application would like to get the system gesture exclusion rects. */ - List getSystemGestureExclusionRects(); - - /** - * The Flutter application would like to set the system gesture exclusion rects through the - * given {@code rects}. - */ - void setSystemGestureExclusionRects(@NonNull ArrayList rects); } /** Types of sounds the Android OS can play on behalf of an application. */ diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 0fda81fd8930c..d1b58039fb910 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -21,6 +21,8 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import io.flutter.Log; @@ -35,10 +37,58 @@ class InputConnectionAdaptor extends BaseInputConnection { private int mBatchCount; private InputMethodManager mImm; private final Layout mLayout; - // Used to determine if Samsung-specific hacks should be applied. private final boolean isSamsung; + private boolean mRepeatCheckNeeded = false; + private TextEditingValue mLastSentTextEditngValue; + // Data class used to get and store the last-sent values via updateEditingState to + // the framework. These are then compared against to prevent redundant messages + // with the same data before any valid operations were made to the contents. + private class TextEditingValue { + public int selectionStart; + public int selectionEnd; + public int composingStart; + public int composingEnd; + public String text; + + public TextEditingValue(Editable editable) { + selectionStart = Selection.getSelectionStart(editable); + selectionEnd = Selection.getSelectionEnd(editable); + composingStart = BaseInputConnection.getComposingSpanStart(editable); + composingEnd = BaseInputConnection.getComposingSpanEnd(editable); + text = editable.toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof TextEditingValue)) { + return false; + } + TextEditingValue value = (TextEditingValue) o; + return selectionStart == value.selectionStart + && selectionEnd == value.selectionEnd + && composingStart == value.composingStart + && composingEnd == value.composingEnd + && text.equals(value.text); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + selectionStart; + result = prime * result + selectionEnd; + result = prime * result + composingStart; + result = prime * result + composingEnd; + result = prime * result + text.hashCode(); + return result; + } + } + @SuppressWarnings("deprecation") public InputConnectionAdaptor( View view, @@ -74,15 +124,42 @@ private void updateEditingState() { // If the IME is in the middle of a batch edit, then wait until it completes. if (mBatchCount > 0) return; - int selectionStart = Selection.getSelectionStart(mEditable); - int selectionEnd = Selection.getSelectionEnd(mEditable); - int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); - int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); + TextEditingValue currentValue = new TextEditingValue(mEditable); + + // Return if this data has already been sent and no meaningful changes have + // occurred to mark this as dirty. This prevents duplicate remote updates of + // the same data, which can break formatters that change the length of the + // contents. + if (mRepeatCheckNeeded && currentValue.equals(mLastSentTextEditngValue)) { + return; + } - mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd); + mImm.updateSelection( + mFlutterView, + currentValue.selectionStart, + currentValue.selectionEnd, + currentValue.composingStart, + currentValue.composingEnd); textInputChannel.updateEditingState( - mClient, mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd); + mClient, + currentValue.text, + currentValue.selectionStart, + currentValue.selectionEnd, + currentValue.composingStart, + currentValue.composingEnd); + + mRepeatCheckNeeded = true; + mLastSentTextEditngValue = currentValue; + } + + // This should be called whenever a change could have been made to + // the value of mEditable, which will make any call of updateEditingState() + // ineligible for repeat checking as we do not want to skip sending real changes + // to the framework. + public void markDirty() { + // Disable updateEditngState's repeat-update check + mRepeatCheckNeeded = false; } @Override @@ -107,7 +184,7 @@ public boolean endBatchEdit() { @Override public boolean commitText(CharSequence text, int newCursorPosition) { boolean result = super.commitText(text, newCursorPosition); - updateEditingState(); + markDirty(); return result; } @@ -116,14 +193,21 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); - updateEditingState(); + markDirty(); + return result; + } + + @Override + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); + markDirty(); return result; } @Override public boolean setComposingRegion(int start, int end) { boolean result = super.setComposingRegion(start, end); - updateEditingState(); + markDirty(); return result; } @@ -135,7 +219,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { } else { result = super.setComposingText(text, newCursorPosition); } - updateEditingState(); + markDirty(); return result; } @@ -155,28 +239,26 @@ public boolean finishComposingText() { CursorAnchorInfo anchorInfo = builder.build(); mImm.updateCursorAnchorInfo(mFlutterView, anchorInfo); } - // TODO(garyq): There is still a duplication case that comes from hiding+showing the keyboard. - // The exact behavior to cause it has so far been hard to pinpoint and it happens far more - // rarely than the original bug. - - // Temporarily indicate to the IME that the composing region selection should be reset. - // The correct selection is then immediately set properly in the updateEditingState() call - // in this method. This is a hack to trigger Samsung keyboard's internal cache to clear. - // This prevents duplication on keyboard hide+show. See - // https://github.com/flutter/flutter/issues/31512 - // - // We only do this if the proper selection will be restored later, eg, when mBatchCount is 0. - if (mBatchCount == 0) { - mImm.updateSelection( - mFlutterView, - -1, /*selStart*/ - -1, /*selEnd*/ - -1, /*candidatesStart*/ - -1 /*candidatesEnd*/); - } } - updateEditingState(); + markDirty(); + return result; + } + + // TODO(garyq): Implement a more feature complete version of getExtractedText + @Override + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + ExtractedText extractedText = new ExtractedText(); + extractedText.selectionStart = Selection.getSelectionStart(mEditable); + extractedText.selectionEnd = Selection.getSelectionEnd(mEditable); + extractedText.text = mEditable.toString(); + return extractedText; + } + + @Override + public boolean clearMetaKeyStates(int states) { + boolean result = super.clearMetaKeyStates(states); + markDirty(); return result; } @@ -204,6 +286,7 @@ private boolean isSamsung() { @Override public boolean setSelection(int start, int end) { boolean result = super.setSelection(start, end); + markDirty(); updateEditingState(); return result; } @@ -226,6 +309,7 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { + markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable); @@ -351,6 +435,7 @@ public boolean sendKeyEvent(KeyEvent event) { @Override public boolean performContextMenuAction(int id) { + markDirty(); if (id == android.R.id.selectAll) { setSelection(0, mEditable.length()); return true; @@ -404,6 +489,7 @@ public boolean performContextMenuAction(int id) { @Override public boolean performEditorAction(int actionCode) { + markDirty(); switch (actionCode) { case EditorInfo.IME_ACTION_NONE: textInputChannel.newline(mClient); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 9d41dc5b52902..17ccea06728de 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -322,6 +322,10 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) { } // Always apply state to selection which handles updating the selection if needed. applyStateToSelection(state); + InputConnection connection = getLastInputConnection(); + if (connection != null && connection instanceof InputConnectionAdaptor) { + ((InputConnectionAdaptor) connection).markDirty(); + } // Use updateSelection to update imm on selection if it is not neccessary to restart. if (!restartAlwaysRequired && !mRestartInputPending) { mImm.updateSelection( diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 2c88dce423938..38f2b275fd275 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -9,7 +9,6 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.graphics.Rect; import android.os.Build; import android.view.HapticFeedbackConstants; import android.view.SoundEffectConstants; @@ -19,7 +18,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import java.util.ArrayList; import java.util.List; /** Android implementation of the platform plugin. */ @@ -87,16 +85,6 @@ public CharSequence getClipboardData( public void setClipboardData(@NonNull String text) { PlatformPlugin.this.setClipboardData(text); } - - @Override - public List getSystemGestureExclusionRects() { - return PlatformPlugin.this.getSystemGestureExclusionRects(); - } - - @Override - public void setSystemGestureExclusionRects(@NonNull ArrayList rects) { - PlatformPlugin.this.setSystemGestureExclusionRects(rects); - } }; public PlatformPlugin(Activity activity, PlatformChannel platformChannel) { @@ -299,24 +287,4 @@ private void setClipboardData(String text) { ClipData clip = ClipData.newPlainText("text label?", text); clipboard.setPrimaryClip(clip); } - - private List getSystemGestureExclusionRects() { - if (Build.VERSION.SDK_INT >= 29) { - Window window = activity.getWindow(); - View view = window.getDecorView(); - return view.getSystemGestureExclusionRects(); - } - - return null; - } - - private void setSystemGestureExclusionRects(ArrayList rects) { - if (Build.VERSION.SDK_INT < 29) { - return; - } - - Window window = activity.getWindow(); - View view = window.getDecorView(); - view.setSystemGestureExclusionRects(rects); - } } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 6c87e20c0fd94..cc5dfc91af850 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -404,7 +404,21 @@ public PlatformViewRegistry getRegistry() { return registry; } - public void onFlutterViewDestroyed() { + /** + * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link + * PlatformViewsController} attaches to JNI. + */ + public void onAttachedToJNI() { + // Currently no action needs to be taken after JNI attachment. + } + + /** + * Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link + * PlatformViewsController} detaches from JNI. + */ + public void onDetachedFromJNI() { + // Dispose all virtual displays so that any future updates to textures will not be + // propagated to the native peer. flushAllViews(); } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index 9bda1295b9b88..993f88efb2a8c 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -9,6 +9,7 @@ import static android.view.View.OnFocusChangeListener; import android.annotation.TargetApi; +import android.app.AlertDialog; import android.app.Presentation; import android.content.Context; import android.content.ContextWrapper; @@ -102,6 +103,9 @@ static class PresentationState { private boolean startFocused = false; + // The context for the application window that hosts FlutterView. + private final Context outerContext; + /** * Creates a presentation that will use the view factory to create a new platform view in the * presentation's onCreate, and attach it. @@ -120,11 +124,15 @@ public SingleViewPresentation( this.viewId = viewId; this.createParams = createParams; this.focusChangeListener = focusChangeListener; + this.outerContext = outerContext; state = new PresentationState(); getWindow() .setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION); + } } /** @@ -144,6 +152,7 @@ public SingleViewPresentation( viewFactory = null; this.state = state; this.focusChangeListener = focusChangeListener; + this.outerContext = outerContext; getWindow() .setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, @@ -170,7 +179,8 @@ protected void onCreate(Bundle savedInstanceState) { // Our base mContext has already been wrapped with an IMM cache at instantiation time, but // we want to wrap it again here to also return state.windowManagerHandler. - Context context = new PresentationContext(getContext(), state.windowManagerHandler); + Context context = + new PresentationContext(getContext(), state.windowManagerHandler, outerContext); if (state.platformView == null) { state.platformView = viewFactory.create(context, viewId, createParams); @@ -301,15 +311,34 @@ public Context createDisplayContext(Display display) { private static class PresentationContext extends ContextWrapper { private @NonNull final WindowManagerHandler windowManagerHandler; private @Nullable WindowManager windowManager; + private final Context flutterAppWindowContext; - PresentationContext(Context base, @NonNull WindowManagerHandler windowManagerHandler) { + PresentationContext( + Context base, + @NonNull WindowManagerHandler windowManagerHandler, + Context flutterAppWindowContext) { super(base); this.windowManagerHandler = windowManagerHandler; + this.flutterAppWindowContext = flutterAppWindowContext; } @Override public Object getSystemService(String name) { if (WINDOW_SERVICE.equals(name)) { + if (isCalledFromAlertDialog()) { + // Alert dialogs are showing on top of the entire application and should not be limited to + // the virtual + // display. If we detect that an android.app.AlertDialog constructor is what's fetching + // the window manager + // we return the one for the application's window. + // + // Note that if we don't do this AlertDialog will throw a ClassCastException as down the + // line it tries + // to case this instance to a WindowManagerImpl which the object returned by + // getWindowManager is not + // a subclass of. + return flutterAppWindowContext.getSystemService(name); + } return getWindowManager(); } return super.getSystemService(name); @@ -321,6 +350,17 @@ private WindowManager getWindowManager() { } return windowManager; } + + private boolean isCalledFromAlertDialog() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (int i = 0; i < stackTraceElements.length && i < 11; i++) { + if (stackTraceElements[i].getClassName().equals(AlertDialog.class.getCanonicalName()) + && stackTraceElements[i].getMethodName().equals("")) { + return true; + } + } + return false; + } } /* diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 856fd9748ce90..a58efec690e37 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -48,7 +48,7 @@ void PlatformViewAndroid::NotifyCreated( fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), + task_runners_.GetRasterTaskRunner(), [&latch, surface = android_surface_.get(), native_window = std::move(native_window)]() { surface->SetNativeWindow(native_window); @@ -66,7 +66,7 @@ void PlatformViewAndroid::NotifyDestroyed() { if (android_surface_) { fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), + task_runners_.GetRasterTaskRunner(), [&latch, surface = android_surface_.get()]() { surface->TeardownOnScreenContext(); latch.Signal(); @@ -81,7 +81,7 @@ void PlatformViewAndroid::NotifyChanged(const SkISize& size) { } fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - task_runners_.GetGPUTaskRunner(), // + task_runners_.GetRasterTaskRunner(), // [&latch, surface = android_surface_.get(), size]() { surface->OnScreenSurfaceResize(size); latch.Signal(); diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 480998ac362d4..d793f5a731877 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -4,6 +4,7 @@ package io.flutter; +import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest; import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterAndroidComponentTest; import io.flutter.embedding.android.FlutterFragmentTest; @@ -14,7 +15,6 @@ import io.flutter.embedding.engine.RenderingComponentTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; -import io.flutter.embedding.engine.systemchannels.PlatformChannelTest; import io.flutter.external.FlutterLaunchTests; import io.flutter.plugin.common.StandardMessageCodecTest; import io.flutter.plugin.editing.InputConnectionAdaptorTest; @@ -32,8 +32,8 @@ @RunWith(Suite.class) @SuiteClasses({ - // FlutterActivityAndFragmentDelegateTest.class, //TODO(mklim): Fix and re-enable this DartExecutorTest.class, + FlutterActivityAndFragmentDelegateTest.class, FlutterActivityTest.class, FlutterAndroidComponentTest.class, FlutterEngineCacheTest.class, @@ -46,7 +46,6 @@ FlutterRendererTest.class, FlutterViewTest.class, InputConnectionAdaptorTest.class, - PlatformChannelTest.class, PlatformPluginTest.class, PluginComponentTest.class, PreconditionsTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 3d8f420795f22..71a946f8a8902 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -220,7 +220,18 @@ public void itGivesHostAnOpportunityToConfigureFlutterSurfaceView() { public void itGivesHostAnOpportunityToConfigureFlutterTextureView() { // ---- Test setup ---- Host customMockHost = mock(Host.class); + when(customMockHost.getContext()).thenReturn(RuntimeEnvironment.application); + when(customMockHost.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class)); + when(customMockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); + when(customMockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); + when(customMockHost.getDartEntrypointFunctionName()).thenReturn("main"); + when(customMockHost.getAppBundlePath()).thenReturn("/fake/path"); + when(customMockHost.getInitialRoute()).thenReturn("/"); when(customMockHost.getRenderMode()).thenReturn(RenderMode.texture); + when(customMockHost.getTransparencyMode()).thenReturn(TransparencyMode.transparent); + when(customMockHost.provideFlutterEngine(any(Context.class))).thenReturn(mockFlutterEngine); + when(customMockHost.shouldAttachEngineToActivity()).thenReturn(true); + when(customMockHost.shouldDestroyEngineWithHost()).thenReturn(true); // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java index c95612281f1c8..9a15364373b7a 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -102,6 +102,28 @@ public void itNotifiesPlatformViewsControllerWhenDevHotRestart() { verify(platformViewsController, times(1)).onPreEngineRestart(); } + @Test + public void itNotifiesPlatformViewsControllerAboutJNILifecycle() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + when(mockFlutterJNI.isAttached()).thenReturn(true); + + PlatformViewsController platformViewsController = mock(PlatformViewsController.class); + + // Execute behavior under test. + FlutterEngine engine = + new FlutterEngine( + RuntimeEnvironment.application, + mock(FlutterLoader.class), + mockFlutterJNI, + platformViewsController, + /*dartVmArgs=*/ new String[] {}, + /*automaticallyRegisterPlugins=*/ false); + verify(platformViewsController, times(1)).onAttachedToJNI(); + + engine.destroy(); + verify(platformViewsController, times(1)).onDetachedFromJNI(); + } + @Test public void itUsesApplicationContext() { Context context = mock(Context.class); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java deleted file mode 100644 index ed34a112f8fad..0000000000000 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package io.flutter.embedding.engine.systemchannels; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.graphics.Rect; -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.systemchannels.PlatformChannel.PlatformMessageHandler; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.Result; -import java.util.ArrayList; -import java.util.HashMap; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@Config(manifest = Config.NONE) -@RunWith(RobolectricTestRunner.class) -public class PlatformChannelTest { - @Test - public void itSendsSuccessMessageToFrameworkWhenGettingSystemGestureExclusionRects() - throws JSONException { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - Result result = mock(Result.class); - - // Fake API output setup - ArrayList fakeExclusionRects = new ArrayList(); - Rect gestureRect = new Rect(0, 0, 500, 250); - fakeExclusionRects.add(gestureRect); - when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(fakeExclusionRects); - - // Parsed API output that should be passed to result.success() - ArrayList> expectedEncodedOutputRects = - new ArrayList>(); - HashMap rectMap = new HashMap(); - rectMap.put("top", 0); - rectMap.put("right", 500); - rectMap.put("bottom", 250); - rectMap.put("left", 0); - expectedEncodedOutputRects.add(rectMap); - MethodCall callGetSystemGestureExclusionRects = - new MethodCall("SystemGestures.getSystemGestureExclusionRects", null); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall( - callGetSystemGestureExclusionRects, result); - - // --- Verify Results --- - verify(result, times(1)).success(expectedEncodedOutputRects); - } - - @Test - public void - itSendsAPILevelErrorWhenAndroidVersionIsTooLowWhenGettingSystemGestureExclusionRects() { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(null); - Result result = mock(Result.class); - - MethodCall callGetSystemGestureExclusionRects = - new MethodCall("SystemGestures.getSystemGestureExclusionRects", null); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall( - callGetSystemGestureExclusionRects, result); - - // --- Verify Results --- - verify(result, times(1)) - .error("error", "Exclusion rects only exist for Android API 29+.", null); - } - - @Test - public void itSendsSuccessMessageToFrameworkWhenSettingSystemGestureExclusionRects() - throws JSONException { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - Result result = mock(Result.class); - - JSONObject jsonRect = new JSONObject(); - jsonRect.put("top", 0); - jsonRect.put("right", 500); - jsonRect.put("bottom", 250); - jsonRect.put("left", 0); - JSONArray jsonExclusionRectsFromPlatform = new JSONArray(); - jsonExclusionRectsFromPlatform.put(jsonRect); - - MethodCall callSystemGestureExclusionRects = - new MethodCall( - "SystemGestures.setSystemGestureExclusionRects", jsonExclusionRectsFromPlatform); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall(callSystemGestureExclusionRects, result); - - // --- Verify Results --- - verify(result, times(1)).success(null); - } - - @Test - public void itProperlyDecodesGestureRectsWhenSettingSystemGestureExclusionRects() - throws JSONException { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - Result result = mock(Result.class); - - JSONObject jsonRect = new JSONObject(); - jsonRect.put("top", 0); - jsonRect.put("right", 500); - jsonRect.put("bottom", 250); - jsonRect.put("left", 0); - JSONArray jsonExclusionRectsFromPlatform = new JSONArray(); - jsonExclusionRectsFromPlatform.put(jsonRect); - - ArrayList expectedDecodedRects = new ArrayList(); - Rect gestureRect = new Rect(0, 0, 500, 250); - expectedDecodedRects.add(gestureRect); - - MethodCall callSetSystemGestureExclusionRects = - new MethodCall( - "SystemGestures.setSystemGestureExclusionRects", jsonExclusionRectsFromPlatform); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall( - callSetSystemGestureExclusionRects, result); - - // --- Verify Results --- - verify(platformMessageHandler, times(1)).setSystemGestureExclusionRects(expectedDecodedRects); - } - - @Test - public void itSendsJSONInputErrorWhenNonJSONInputIsUsedWhenSettingSystemGestureExclusionRects() { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - Result result = mock(Result.class); - - String nonJsonInput = "Non-JSON"; - MethodCall callSetSystemGestureExclusionRects = - new MethodCall("SystemGestures.setSystemGestureExclusionRects", nonJsonInput); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall( - callSetSystemGestureExclusionRects, result); - - // --- Verify Results --- - String inputTypeError = - "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; - verify(result, times(1)).error("inputTypeError", inputTypeError, null); - } - - @Test - public void itSendsJSONErrorWhenIncorrectJSONShapeIsUsedWhenSettingSystemGestureExclusionRects() - throws JSONException { - // --- Test Setup --- - DartExecutor dartExecutor = mock(DartExecutor.class); - PlatformChannel platformChannel = new PlatformChannel(dartExecutor); - PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); - platformChannel.setPlatformMessageHandler(platformMessageHandler); - Result result = mock(Result.class); - - // Add key/value pairs that aren't needed by exclusion rects to simulate incorrect JSON shape - JSONObject jsonObject = new JSONObject(); - jsonObject.put("arg1", 0); - jsonObject.put("arg2", 500); - JSONArray inputArray = new JSONArray(); - inputArray.put(jsonObject); - - MethodCall callSetSystemGestureExclusionRects = - new MethodCall("SystemGestures.setSystemGestureExclusionRects", inputArray); - - // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall( - callSetSystemGestureExclusionRects, result); - - // --- Verify Results --- - verify(result, times(1)) - .error( - "error", - "JSON error: Incorrect JSON data shape. To set system gesture exclusion rects, \n" - + "a JSONObject with top, right, bottom and left values need to be set to int values.", - null); - } -} diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index 9d8d95095adf5..bf0f513a0835e 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -19,6 +19,7 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -256,6 +257,62 @@ public void testSendKeyEvent_downKeyMovesCaretDown() { assertTrue(Selection.getSelectionStart(editable) > selStart); } + @Test + public void testMethod_getExtractedText() { + int selStart = 5; + Editable editable = sampleEditable(selStart, selStart); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); + + ExtractedText extractedText = adaptor.getExtractedText(null, 0); + + assertEquals(extractedText.text, SAMPLE_TEXT); + assertEquals(extractedText.selectionStart, selStart); + assertEquals(extractedText.selectionEnd, selStart); + } + + @Test + public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { + View testView = new View(RuntimeEnvironment.application); + FlutterJNI mockFlutterJni = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); + int inputTargetId = 0; + TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor); + Editable mEditable = Editable.Factory.getInstance().newEditable(""); + Editable spyEditable = spy(mEditable); + EditorInfo outAttrs = new EditorInfo(); + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; + + InputConnectionAdaptor inputConnectionAdaptor = + new InputConnectionAdaptor( + testView, inputTargetId, textInputChannel, spyEditable, outAttrs); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 0); + inputConnectionAdaptor.setComposingText("I do not fear computers. I fear the lack of them.", 1); + assertEquals(textInputChannel.text, null); + assertEquals(textInputChannel.updateEditingStateInvocations, 0); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + inputConnectionAdaptor.setSelection(3, 4); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + assertEquals(textInputChannel.selectionStart, 49); + assertEquals(textInputChannel.selectionEnd, 49); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 2); + assertEquals(textInputChannel.selectionStart, 3); + assertEquals(textInputChannel.selectionEnd, 4); + } + private static final String SAMPLE_TEXT = "Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit."; @@ -271,4 +328,35 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit TextInputChannel textInputChannel = mock(TextInputChannel.class); return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null); } + + private class TestTextInputChannel extends TextInputChannel { + public TestTextInputChannel(DartExecutor dartExecutor) { + super(dartExecutor); + } + + public int inputClientId; + public String text; + public int selectionStart; + public int selectionEnd; + public int composingStart; + public int composingEnd; + public int updateEditingStateInvocations = 0; + + @Override + public void updateEditingState( + int inputClientId, + String text, + int selectionStart, + int selectionEnd, + int composingStart, + int composingEnd) { + this.inputClientId = inputClientId; + this.text = text; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + this.composingStart = composingStart; + this.composingEnd = composingEnd; + updateEditingStateInvocations++; + } + } } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 51dbc603b9793..b2ecfd7b13fd6 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -29,8 +29,6 @@ import io.flutter.plugin.platform.PlatformViewsController; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.junit.Test; @@ -349,48 +347,6 @@ public void inputConnection_finishComposingTextUpdatesIMM() throws JSONException } } - @Test - public void inputConnection_samsungFinishComposingTextSetsSelection() throws JSONException { - ShadowBuild.setManufacturer("samsung"); - InputMethodSubtype inputMethodSubtype = - new InputMethodSubtype(0, 0, /*locale=*/ "en", "", "", false, false); - Settings.Secure.putString( - RuntimeEnvironment.application.getContentResolver(), - Settings.Secure.DEFAULT_INPUT_METHOD, - "com.sec.android.inputmethod/.SamsungKeypad"); - TestImm testImm = - Shadow.extract( - RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); - testImm.setCurrentInputMethodSubtype(inputMethodSubtype); - FlutterJNI mockFlutterJni = mock(FlutterJNI.class); - View testView = new View(RuntimeEnvironment.application); - DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); - TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, dartExecutor, mock(PlatformViewsController.class)); - textInputPlugin.setTextInputClient( - 0, - new TextInputChannel.Configuration( - false, - false, - true, - TextInputChannel.TextCapitalization.NONE, - new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false), - null, - null)); - // There's a pending restart since we initialized the text input client. Flush that now. - textInputPlugin.setTextInputEditingState( - testView, new TextInputChannel.TextEditState("", 0, 0)); - InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); - - testImm.setTrackSelection(true); - connection.finishComposingText(); - testImm.setTrackSelection(false); - - List expectedSelectionValues = - Arrays.asList(0, 0, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1); - assertEquals(testImm.getSelectionUpdateValues(), expectedSelectionValues); - } - @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; diff --git a/shell/platform/common/cpp/BUILD.gn b/shell/platform/common/cpp/BUILD.gn index b4d5b0a5bf88b..f3bcde6b2442d 100644 --- a/shell/platform/common/cpp/BUILD.gn +++ b/shell/platform/common/cpp/BUILD.gn @@ -2,6 +2,8 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//flutter/testing/testing.gni") + config("desktop_library_implementation") { defines = [ "FLUTTER_DESKTOP_LIBRARY" ] } @@ -31,6 +33,8 @@ source_set("common_cpp_library_headers") { source_set("common_cpp") { public = [ "incoming_message_dispatcher.h", + "json_message_codec.h", + "json_method_codec.h", "text_input_model.h", ] @@ -38,6 +42,8 @@ source_set("common_cpp") { # to the _public_headers above into this target. sources = [ "incoming_message_dispatcher.cc", + "json_message_codec.cc", + "json_method_codec.cc", "text_input_model.cc", ] @@ -49,17 +55,75 @@ source_set("common_cpp") { "//flutter/shell/platform/embedder:embedder_with_symbol_prefix", ] - # TODO: Remove once text input model refactor lands, at which point this code - # won't have a JSON dependency. - defines = [ "USE_RAPID_JSON" ] - deps += [ "//third_party/rapidjson" ] + public_deps = [ + ":common_cpp_core", + "//third_party/rapidjson", + ] if (is_win) { # For wstring_conversion. See issue #50053. - defines += [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ] + defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ] } } +# The portion of common_cpp that has no dependencies on the public/ +# headers. This division should be revisited once the Linux GTK +# embedding is futher along and it's clearer how much, if any, shared +# API surface there will be. +source_set("common_cpp_core") { + public = [ + "path_utils.h", + ] + + sources = [ + "path_utils.cc", + ] +} + +test_fixtures("common_cpp_core_fixtures") { + fixtures = [] +} + +executable("common_cpp_core_unittests") { + testonly = true + + sources = [ + "path_utils_unittests.cc", + ] + + deps = [ + ":common_cpp_core", + ":common_cpp_core_fixtures", + "//flutter/testing", + "//third_party/dart/runtime:libdart_jit", + ] + + public_configs = [ "//flutter:config" ] +} + +test_fixtures("common_cpp_fixtures") { + fixtures = [] +} + +executable("common_cpp_unittests") { + testonly = true + + sources = [ + "json_message_codec_unittests.cc", + "json_method_codec_unittests.cc", + ] + + deps = [ + ":common_cpp", + ":common_cpp_fixtures", + "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper", + "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper_library_stubs", + "//flutter/testing", + ] + + public_configs = [ "//flutter:config" ] +} + copy("publish_headers") { sources = _public_headers outputs = [ diff --git a/shell/platform/common/cpp/client_wrapper/BUILD.gn b/shell/platform/common/cpp/client_wrapper/BUILD.gn index a8d30109d2856..94e68dc86084a 100644 --- a/shell/platform/common/cpp/client_wrapper/BUILD.gn +++ b/shell/platform/common/cpp/client_wrapper/BUILD.gn @@ -12,11 +12,8 @@ source_set("client_wrapper") { deps = [ "//flutter/shell/platform/common/cpp:common_cpp_library_headers", - "//third_party/rapidjson", ] - defines = [ "USE_RAPID_JSON" ] - configs += [ "//flutter/shell/platform/common/cpp:desktop_library_implementation" ] @@ -44,12 +41,12 @@ test_fixtures("client_wrapper_fixtures") { executable("client_wrapper_unittests") { testonly = true - # TODO: Add more unit tests. sources = [ "basic_message_channel_unittests.cc", "encodable_value_unittests.cc", "method_call_unittests.cc", "method_channel_unittests.cc", + "method_result_functions_unittests.cc", "plugin_registrar_unittests.cc", "standard_message_codec_unittests.cc", "standard_method_codec_unittests.cc", @@ -68,4 +65,6 @@ executable("client_wrapper_unittests") { # https://github.com/flutter/flutter/issues/41414. "//third_party/dart/runtime:libdart_jit", ] + + defines = [ "FLUTTER_DESKTOP_LIBRARY" ] } diff --git a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc index 3e47b80ceca45..5098e89b30dd6 100644 --- a/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/basic_message_channel_unittests.cc @@ -17,10 +17,6 @@ namespace { class TestBinaryMessenger : public BinaryMessenger { public: - void Send(const std::string& channel, - const uint8_t* message, - const size_t message_size) const override {} - void Send(const std::string& channel, const uint8_t* message, const size_t message_size, diff --git a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni index ce91be7866717..86c3280ea3699 100644 --- a/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni +++ b/shell/platform/common/cpp/client_wrapper/core_wrapper_files.gni @@ -8,13 +8,11 @@ core_cpp_client_wrapper_includes = "include/flutter/binary_messenger.h", "include/flutter/encodable_value.h", "include/flutter/engine_method_result.h", - "include/flutter/json_message_codec.h", - "include/flutter/json_method_codec.h", - "include/flutter/json_type.h", "include/flutter/message_codec.h", "include/flutter/method_call.h", "include/flutter/method_channel.h", "include/flutter/method_codec.h", + "include/flutter/method_result_functions.h", "include/flutter/method_result.h", "include/flutter/plugin_registrar.h", "include/flutter/plugin_registry.h", @@ -26,15 +24,11 @@ core_cpp_client_wrapper_includes = # TODO: Once the wrapper API is more stable, consolidate to as few files as is # reasonable (without forcing different kinds of clients to take unnecessary # code) to simplify use. -core_cpp_client_wrapper_sources = - get_path_info( - [ - "byte_stream_wrappers.h", - "engine_method_result.cc", - "json_message_codec.cc", # TODO combine into a single json_codec.cc. - "json_method_codec.cc", # TODO combine into a single json_codec.cc. - "plugin_registrar.cc", - "standard_codec_serializer.h", - "standard_codec.cc", - ], - "abspath") +core_cpp_client_wrapper_sources = get_path_info([ + "byte_stream_wrappers.h", + "engine_method_result.cc", + "plugin_registrar.cc", + "standard_codec_serializer.h", + "standard_codec.cc", + ], + "abspath") diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h b/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h index 08d074c86e744..8dd2f78ad8f76 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h @@ -8,21 +8,19 @@ #include #include -// TODO: Consider adding absl as a dependency and using absl::Span for all of -// the message/message_size pairs. namespace flutter { // A binary message reply callback. // // Used for submitting a binary reply back to a Flutter message sender. -typedef std::function +typedef std::function BinaryReply; // A message handler callback. // // Used for receiving messages from Flutter and providing an asynchronous reply. typedef std::function< - void(const uint8_t* message, const size_t message_size, BinaryReply reply)> + void(const uint8_t* message, size_t message_size, BinaryReply reply)> BinaryMessageHandler; // A protocol for a class that handles communication of binary data on named @@ -31,18 +29,14 @@ class BinaryMessenger { public: virtual ~BinaryMessenger() = default; - // Sends a binary message to the Flutter side on the specified channel, - // expecting no reply. - virtual void Send(const std::string& channel, - const uint8_t* message, - const size_t message_size) const = 0; - - // Sends a binary message to the Flutter side on the specified channel, - // expecting a reply. + // Sends a binary message to the Flutter engine on the specified channel. + // + // If |reply| is provided, it will be called back with the response from the + // engine. virtual void Send(const std::string& channel, const uint8_t* message, - const size_t message_size, - BinaryReply reply) const = 0; + size_t message_size, + BinaryReply reply = nullptr) const = 0; // Registers a message handler for incoming binary messages from the Flutter // side on the specified channel. diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/json_type.h b/shell/platform/common/cpp/client_wrapper/include/flutter/json_type.h deleted file mode 100644 index 24baf1328d668..0000000000000 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/json_type.h +++ /dev/null @@ -1,26 +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_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_TYPE_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_TYPE_H_ - -// By default, the Json codecs use jsoncpp, but a version using RapidJSON is -// implemented as well. To use the latter, set USE_RAPID_JSON. -// -// When writing code using the JSON codec classes, do not use JsonValueType; -// instead use the underlying type for the library you have selected directly. - -#ifdef USE_RAPID_JSON -#include - -// The APIs often pass owning references, which in RapidJSON must include the -// allocator, so the value type for the APIs is Document rather than Value. -using JsonValueType = rapidjson::Document; -#else -#include - -using JsonValueType = Json::Value; -#endif - -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_TYPE_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h index 96658af96c3c4..0fe94701c963b 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_channel.h @@ -44,21 +44,46 @@ class MethodChannel { MethodChannel& operator=(MethodChannel const&) = delete; // Sends a message to the Flutter engine on this channel. - void InvokeMethod(const std::string& method, std::unique_ptr arguments) { - MethodCall method_call(method, std::move(arguments)); - std::unique_ptr> message = - codec_->EncodeMethodCall(method_call); - messenger_->Send(name_, message->data(), message->size(), nullptr); - } - - // Sends a message to the Flutter engine on this channel expecting a reply. + // + // If |result| is provided, one of its methods will be invoked with the + // response from the engine. void InvokeMethod(const std::string& method, std::unique_ptr arguments, - flutter::BinaryReply reply) { + std::unique_ptr> result = nullptr) { MethodCall method_call(method, std::move(arguments)); std::unique_ptr> message = codec_->EncodeMethodCall(method_call); - messenger_->Send(name_, message->data(), message->size(), reply); + if (!result) { + messenger_->Send(name_, message->data(), message->size(), nullptr); + return; + } + + // std::function requires a copyable lambda, so convert to a shared pointer. + // This is safe since only one copy of the shared_pointer will ever be + // accessed. + std::shared_ptr> shared_result(result.release()); + const auto* codec = codec_; + std::string channel_name = name_; + BinaryReply reply_handler = [shared_result, codec, channel_name]( + const uint8_t* reply, size_t reply_size) { + if (reply_size == 0) { + shared_result->NotImplemented(); + return; + } + // Use this channel's codec to decode and handle the + // reply. + bool decoded = codec->DecodeAndProcessResponseEnvelope( + reply, reply_size, shared_result.get()); + if (!decoded) { + std::cerr << "Unable to decode reply to method " + "invocation on channel " + << channel_name << std::endl; + shared_result->NotImplemented(); + } + }; + + messenger_->Send(name_, message->data(), message->size(), + std::move(reply_handler)); } // Registers a handler that should be called any time a method call is @@ -76,7 +101,7 @@ class MethodChannel { std::string channel_name = name_; BinaryMessageHandler binary_handler = [handler, codec, channel_name]( const uint8_t* message, - const size_t message_size, + size_t message_size, BinaryReply reply) { // Use this channel's codec to decode the call and build a result handler. auto result = diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h index 6f9e09ac24775..959e298c5984a 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h @@ -10,6 +10,7 @@ #include #include "method_call.h" +#include "method_result.h" namespace flutter { @@ -28,9 +29,8 @@ class MethodCodec { // Returns the MethodCall encoded in |message|, or nullptr if it cannot be // decoded. - std::unique_ptr> DecodeMethodCall( - const uint8_t* message, - const size_t message_size) const { + std::unique_ptr> DecodeMethodCall(const uint8_t* message, + size_t message_size) const { return std::move(DecodeMethodCallInternal(message, message_size)); } @@ -67,11 +67,23 @@ class MethodCodec { EncodeErrorEnvelopeInternal(error_code, error_message, error_details)); } + // Decodes the response envelope encoded in |response|, calling the + // appropriate method on |result|. + // + // Returns false if |response| cannot be decoded. In that case the caller is + // responsible for calling a |result| method. + bool DecodeAndProcessResponseEnvelope(const uint8_t* response, + size_t response_size, + MethodResult* result) const { + return DecodeAndProcessResponseEnvelopeInternal(response, response_size, + result); + } + protected: // Implementation of the public interface, to be provided by subclasses. virtual std::unique_ptr> DecodeMethodCallInternal( const uint8_t* message, - const size_t message_size) const = 0; + size_t message_size) const = 0; // Implementation of the public interface, to be provided by subclasses. virtual std::unique_ptr> EncodeMethodCallInternal( @@ -86,6 +98,12 @@ class MethodCodec { const std::string& error_code, const std::string& error_message, const T* error_details) const = 0; + + // Implementation of the public interface, to be provided by subclasses. + virtual bool DecodeAndProcessResponseEnvelopeInternal( + const uint8_t* response, + size_t response_size, + MethodResult* result) const = 0; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h index e3bf572996eb4..1e9c822e253e8 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result.h @@ -9,8 +9,8 @@ namespace flutter { -// Encapsulates a result sent back to the Flutter engine in response to a -// MethodCall. Only one method should be called on any given instance. +// Encapsulates a result returned from a MethodCall. Only one method should be +// called on any given instance. template class MethodResult { public: diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h new file mode 100644 index 0000000000000..762128f444324 --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h @@ -0,0 +1,77 @@ +// 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_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_METHOD_RESULT_FUNCTIONS_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_METHOD_RESULT_FUNCTIONS_H_ + +#include +#include + +#include "method_result.h" + +namespace flutter { + +// Handler types for each of the MethodResult outcomes. +template +using ResultHandlerSuccess = std::function; +template +using ResultHandlerError = std::function; +template +using ResultHandlerNotImplemented = std::function; + +// An implementation of MethodResult that pass calls through to provided +// function objects, for ease of constructing one-off result handlers. +template +class MethodResultFunctions : public MethodResult { + public: + // Creates a result object that calls the provided functions for the + // corresponding MethodResult outcomes. + MethodResultFunctions(ResultHandlerSuccess on_success, + ResultHandlerError on_error, + ResultHandlerNotImplemented on_not_implemented) + : on_success_(on_success), + on_error_(on_error), + on_not_implemented_(on_not_implemented) {} + + virtual ~MethodResultFunctions() = default; + + // Prevent copying. + MethodResultFunctions(MethodResultFunctions const&) = delete; + MethodResultFunctions& operator=(MethodResultFunctions const&) = delete; + + protected: + // |flutter::MethodResult| + void SuccessInternal(const T* result) override { + if (on_success_) { + on_success_(result); + } + } + + // |flutter::MethodResult| + void ErrorInternal(const std::string& error_code, + const std::string& error_message, + const T* error_details) override { + if (on_error_) { + on_error_(error_code, error_message, error_details); + } + } + + // |flutter::MethodResult| + void NotImplementedInternal() override { + if (on_not_implemented_) { + on_not_implemented_(); + } + } + + private: + ResultHandlerSuccess on_success_; + ResultHandlerError on_error_; + ResultHandlerNotImplemented on_not_implemented_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_METHOD_RESULT_FUNCTIONS_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h index 5fbe1b5b6297c..647b7cbb48e8b 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ #define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ +#include #include #include #include @@ -69,6 +70,53 @@ class Plugin { virtual ~Plugin() = default; }; +// A singleton to own PluginRegistrars. This is intended for use in plugins, +// where there is no higher-level object to own a PluginRegistrar that can +// own plugin instances and ensure that they live as long as the engine they +// are registered with. +class PluginRegistrarManager { + public: + static PluginRegistrarManager* GetInstance(); + + // Prevent copying. + PluginRegistrarManager(PluginRegistrarManager const&) = delete; + PluginRegistrarManager& operator=(PluginRegistrarManager const&) = delete; + + // Returns a plugin registrar wrapper of type T, which must be a kind of + // PluginRegistrar, creating it if necessary. The returned registrar will + // live as long as the underlying FlutterDesktopPluginRegistrarRef, so + // can be used to own plugin instances. + // + // Calling this multiple times for the same registrar_ref with different + // template types results in undefined behavior. + template + T* GetRegistrar(FlutterDesktopPluginRegistrarRef registrar_ref) { + auto insert_result = + registrars_.emplace(registrar_ref, std::make_unique(registrar_ref)); + auto& registrar_pair = *(insert_result.first); + FlutterDesktopRegistrarSetDestructionHandler(registrar_pair.first, + OnRegistrarDestroyed); + return static_cast(registrar_pair.second.get()); + } + + // Destroys all registrar wrappers created by the manager. + // + // This is intended primarily for use in tests. + void Reset() { registrars_.clear(); } + + private: + PluginRegistrarManager(); + + using WrapperMap = std::map>; + + static void OnRegistrarDestroyed(FlutterDesktopPluginRegistrarRef registrar); + + WrapperMap* registrars() { return ®istrars_; } + + WrapperMap registrars_; +}; + } // namespace flutter #endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_PLUGIN_REGISTRAR_H_ diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h index 3b474110613cb..ef40897893183 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h +++ b/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h @@ -30,7 +30,7 @@ class StandardMethodCodec : public MethodCodec { // |flutter::MethodCodec| std::unique_ptr> DecodeMethodCallInternal( const uint8_t* message, - const size_t message_size) const override; + size_t message_size) const override; // |flutter::MethodCodec| std::unique_ptr> EncodeMethodCallInternal( @@ -45,6 +45,12 @@ class StandardMethodCodec : public MethodCodec { const std::string& error_code, const std::string& error_message, const EncodableValue* error_details) const override; + + // |flutter::MethodCodec| + bool DecodeAndProcessResponseEnvelopeInternal( + const uint8_t* response, + size_t response_size, + MethodResult* result) const override; }; } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc index 549c1023a4ab0..ee62b526eaf12 100644 --- a/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/method_channel_unittests.cc @@ -8,6 +8,7 @@ #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/binary_messenger.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" #include "gtest/gtest.h" @@ -19,12 +20,11 @@ class TestBinaryMessenger : public BinaryMessenger { public: void Send(const std::string& channel, const uint8_t* message, - const size_t message_size) const override {} - - void Send(const std::string& channel, - const uint8_t* message, - const size_t message_size, - BinaryReply reply) const override {} + size_t message_size, + BinaryReply reply) const override { + send_called_ = true; + last_reply_handler_ = reply; + } void SetMessageHandler(const std::string& channel, BinaryMessageHandler handler) override { @@ -32,6 +32,10 @@ class TestBinaryMessenger : public BinaryMessenger { last_message_handler_ = handler; } + bool send_called() { return send_called_; } + + BinaryReply last_reply_handler() { return last_reply_handler_; } + std::string last_message_handler_channel() { return last_message_handler_channel_; } @@ -39,6 +43,8 @@ class TestBinaryMessenger : public BinaryMessenger { BinaryMessageHandler last_message_handler() { return last_message_handler_; } private: + mutable bool send_called_ = false; + mutable BinaryReply last_reply_handler_; std::string last_message_handler_channel_; BinaryMessageHandler last_message_handler_; }; @@ -71,8 +77,8 @@ TEST(MethodChannelTest, Registration) { messenger.last_message_handler()( message->data(), message->size(), - [](const uint8_t* reply, const size_t reply_size) {}); - EXPECT_EQ(callback_called, true); + [](const uint8_t* reply, size_t reply_size) {}); + EXPECT_TRUE(callback_called); } // Tests that SetMethodCallHandler with a null handler unregisters the handler. @@ -91,4 +97,63 @@ TEST(MethodChannelTest, Unregistration) { EXPECT_EQ(messenger.last_message_handler(), nullptr); } +TEST(MethodChannelTest, InvokeWithoutResponse) { + TestBinaryMessenger messenger; + const std::string channel_name("some_channel"); + MethodChannel channel(&messenger, channel_name, + &flutter::StandardMethodCodec::GetInstance()); + + channel.InvokeMethod("foo", nullptr); + EXPECT_TRUE(messenger.send_called()); + EXPECT_EQ(messenger.last_reply_handler(), nullptr); +} + +TEST(MethodChannelTest, InvokeWithResponse) { + TestBinaryMessenger messenger; + const std::string channel_name("some_channel"); + MethodChannel channel(&messenger, channel_name, + &flutter::StandardMethodCodec::GetInstance()); + + bool received_reply = false; + const std::string reply = "bar"; + auto result_handler = std::make_unique>( + [&received_reply, reply](const EncodableValue* success_value) { + received_reply = true; + EXPECT_EQ(success_value->StringValue(), reply); + }, + nullptr, nullptr); + + channel.InvokeMethod("foo", nullptr, std::move(result_handler)); + EXPECT_TRUE(messenger.send_called()); + ASSERT_NE(messenger.last_reply_handler(), nullptr); + + // Call the underlying reply handler to ensure it's processed correctly. + EncodableValue reply_value(reply); + std::unique_ptr> encoded_reply = + flutter::StandardMethodCodec::GetInstance().EncodeSuccessEnvelope( + &reply_value); + messenger.last_reply_handler()(encoded_reply->data(), encoded_reply->size()); + EXPECT_TRUE(received_reply); +} + +TEST(MethodChannelTest, InvokeNotImplemented) { + TestBinaryMessenger messenger; + const std::string channel_name("some_channel"); + MethodChannel channel(&messenger, channel_name, + &flutter::StandardMethodCodec::GetInstance()); + + bool received_not_implemented = false; + auto result_handler = std::make_unique>( + nullptr, nullptr, + [&received_not_implemented]() { received_not_implemented = true; }); + + channel.InvokeMethod("foo", nullptr, std::move(result_handler)); + EXPECT_EQ(messenger.send_called(), true); + ASSERT_NE(messenger.last_reply_handler(), nullptr); + + // Call the underlying reply handler to ensure it's reported as unimplemented. + messenger.last_reply_handler()(nullptr, 0); + EXPECT_TRUE(received_not_implemented); +} + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc new file mode 100644 index 0000000000000..98750dbba244b --- /dev/null +++ b/shell/platform/common/cpp/client_wrapper/method_result_functions_unittests.cc @@ -0,0 +1,66 @@ +// 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/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" + +#include +#include + +#include "gtest/gtest.h" + +namespace flutter { + +// Tests that unset handlers don't cause crashes. +TEST(MethodChannelTest, NoHandlers) { + MethodResultFunctions result(nullptr, nullptr, nullptr); + result.Success(); + result.Error("error"); + result.NotImplemented(); +} + +// Tests that Success calls through to handler. +TEST(MethodChannelTest, Success) { + bool called = false; + int value = 1; + MethodResultFunctions result( + [&called, value](const int* i) { + called = true; + EXPECT_EQ(*i, value); + }, + nullptr, nullptr); + result.Success(&value); + EXPECT_TRUE(called); +} + +// Tests that Error calls through to handler. +TEST(MethodChannelTest, Error) { + bool called = false; + std::string error_code = "a"; + std::string error_message = "b"; + int error_details = 1; + MethodResultFunctions result( + nullptr, + [&called, error_code, error_message, error_details]( + const std::string& code, const std::string& message, + const int* details) { + called = true; + EXPECT_EQ(code, error_code); + EXPECT_EQ(message, error_message); + EXPECT_EQ(*details, error_details); + }, + nullptr); + result.Error(error_code, error_message, &error_details); + EXPECT_TRUE(called); +} + +// Tests that NotImplemented calls through to handler. +TEST(MethodChannelTest, NotImplemented) { + bool called = false; + MethodResultFunctions result(nullptr, nullptr, + [&called]() { called = true; }); + result.NotImplemented(); + EXPECT_TRUE(called); +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc index 7ca7c3de73b1c..9527dd73ead3d 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar.cc @@ -26,7 +26,7 @@ void ForwardToHandler(FlutterDesktopMessengerRef messenger, auto* response_handle = message->response_handle; BinaryReply reply_handler = [messenger, response_handle]( const uint8_t* reply, - const size_t reply_size) mutable { + size_t reply_size) mutable { if (!response_handle) { std::cerr << "Error: Response can be set only once. Ignoring " "duplicate response." @@ -65,12 +65,7 @@ class BinaryMessengerImpl : public BinaryMessenger { // |flutter::BinaryMessenger| void Send(const std::string& channel, const uint8_t* message, - const size_t message_size) const override; - - // |flutter::BinaryMessenger| - void Send(const std::string& channel, - const uint8_t* message, - const size_t message_size, + size_t message_size, BinaryReply reply) const override; // |flutter::BinaryMessenger| @@ -88,14 +83,7 @@ class BinaryMessengerImpl : public BinaryMessenger { void BinaryMessengerImpl::Send(const std::string& channel, const uint8_t* message, - const size_t message_size) const { - FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, - message_size); -} - -void BinaryMessengerImpl::Send(const std::string& channel, - const uint8_t* message, - const size_t message_size, + size_t message_size, BinaryReply reply) const { if (reply == nullptr) { FlutterDesktopMessengerSend(messenger_, channel.c_str(), message, @@ -138,7 +126,7 @@ void BinaryMessengerImpl::SetMessageHandler(const std::string& channel, ForwardToHandler, message_handler); } -// PluginRegistrar: +// ===== PluginRegistrar ===== PluginRegistrar::PluginRegistrar(FlutterDesktopPluginRegistrarRef registrar) : registrar_(registrar) { @@ -157,4 +145,20 @@ void PluginRegistrar::EnableInputBlockingForChannel( FlutterDesktopRegistrarEnableInputBlocking(registrar_, channel.c_str()); } +// ===== PluginRegistrarManager ===== + +// static +PluginRegistrarManager* PluginRegistrarManager::GetInstance() { + static PluginRegistrarManager* instance = new PluginRegistrarManager(); + return instance; +} + +PluginRegistrarManager::PluginRegistrarManager() = default; + +// static +void PluginRegistrarManager::OnRegistrarDestroyed( + FlutterDesktopPluginRegistrarRef registrar) { + GetInstance()->registrars()->erase(registrar); +} + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc b/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc index 5f1a5b4421f5e..58cb1f5fcaa48 100644 --- a/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/plugin_registrar_unittests.cc @@ -33,28 +33,56 @@ class TestApi : public testing::StubFlutterApi { return message_engine_result; } - // Called for FlutterDesktopMessengerSetCallback. void MessengerSetCallback(const char* channel, FlutterDesktopMessageCallback callback, void* user_data) override { - last_callback_set_ = callback; + last_message_callback_set_ = callback; + } + + void RegistrarSetDestructionHandler( + FlutterDesktopOnRegistrarDestroyed callback) override { + last_destruction_callback_set_ = callback; } const uint8_t* last_data_sent() { return last_data_sent_; } - FlutterDesktopMessageCallback last_callback_set() { - return last_callback_set_; + FlutterDesktopMessageCallback last_message_callback_set() { + return last_message_callback_set_; + } + FlutterDesktopOnRegistrarDestroyed last_destruction_callback_set() { + return last_destruction_callback_set_; } private: const uint8_t* last_data_sent_ = nullptr; - FlutterDesktopMessageCallback last_callback_set_ = nullptr; + FlutterDesktopMessageCallback last_message_callback_set_ = nullptr; + FlutterDesktopOnRegistrarDestroyed last_destruction_callback_set_ = nullptr; +}; + +// A PluginRegistrar whose destruction can be watched for by tests. +class TestPluginRegistrar : public PluginRegistrar { + public: + explicit TestPluginRegistrar(FlutterDesktopPluginRegistrarRef core_registrar) + : PluginRegistrar(core_registrar) {} + + virtual ~TestPluginRegistrar() { + if (destruction_callback_) { + destruction_callback_(); + } + } + + void SetDestructionCallback(std::function callback) { + destruction_callback_ = std::move(callback); + } + + private: + std::function destruction_callback_; }; } // namespace // Tests that the registrar returns a messenger that passes Send through to the // C API. -TEST(MethodCallTest, MessengerSend) { +TEST(PluginRegistrarTest, MessengerSend) { testing::ScopedStubFlutterApi scoped_api_stub(std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); @@ -70,7 +98,7 @@ TEST(MethodCallTest, MessengerSend) { // Tests that the registrar returns a messenger that passes callback // registration and unregistration through to the C API. -TEST(MethodCallTest, MessengerSetMessageHandler) { +TEST(PluginRegistrarTest, MessengerSetMessageHandler) { testing::ScopedStubFlutterApi scoped_api_stub(std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); @@ -85,11 +113,70 @@ TEST(MethodCallTest, MessengerSetMessageHandler) { const size_t message_size, BinaryReply reply) {}; messenger->SetMessageHandler(channel_name, std::move(binary_handler)); - EXPECT_NE(test_api->last_callback_set(), nullptr); + EXPECT_NE(test_api->last_message_callback_set(), nullptr); // Unregister. messenger->SetMessageHandler(channel_name, nullptr); - EXPECT_EQ(test_api->last_callback_set(), nullptr); + EXPECT_EQ(test_api->last_message_callback_set(), nullptr); +} + +// Tests that the registrar manager returns the same instance when getting +// the wrapper for the same reference. +TEST(PluginRegistrarTest, ManagerSameInstance) { + PluginRegistrarManager* manager = PluginRegistrarManager::GetInstance(); + manager->Reset(); + + testing::ScopedStubFlutterApi scoped_api_stub(std::make_unique()); + + auto dummy_registrar_handle = + reinterpret_cast(1); + + EXPECT_EQ(manager->GetRegistrar(dummy_registrar_handle), + manager->GetRegistrar(dummy_registrar_handle)); +} + +// Tests that the registrar manager returns different objects for different +// references. +TEST(PluginRegistrarTest, ManagerDifferentInstances) { + PluginRegistrarManager* manager = PluginRegistrarManager::GetInstance(); + manager->Reset(); + + testing::ScopedStubFlutterApi scoped_api_stub(std::make_unique()); + + auto dummy_registrar_handle_a = + reinterpret_cast(1); + auto dummy_registrar_handle_b = + reinterpret_cast(2); + + EXPECT_NE(manager->GetRegistrar(dummy_registrar_handle_a), + manager->GetRegistrar(dummy_registrar_handle_b)); +} + +// Tests that the registrar manager deletes wrappers when the underlying +// reference is destroyed. +TEST(PluginRegistrarTest, ManagerRemovesOnDestruction) { + PluginRegistrarManager* manager = PluginRegistrarManager::GetInstance(); + manager->Reset(); + + testing::ScopedStubFlutterApi scoped_api_stub(std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + auto dummy_registrar_handle = + reinterpret_cast(1); + auto* wrapper = + manager->GetRegistrar(dummy_registrar_handle); + + // Simulate destruction of the reference, and ensure that the wrapper + // is destroyed. + EXPECT_NE(test_api->last_destruction_callback_set(), nullptr); + bool destroyed = false; + wrapper->SetDestructionCallback([&destroyed]() { destroyed = true; }); + test_api->last_destruction_callback_set()(dummy_registrar_handle); + EXPECT_EQ(destroyed, true); + + // Requesting the wrapper should now create a new object. + EXPECT_NE(manager->GetRegistrar(dummy_registrar_handle), + nullptr); } } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/standard_codec.cc b/shell/platform/common/cpp/client_wrapper/standard_codec.cc index 137a3e849002c..1793174819a62 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_codec.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_codec.cc @@ -83,7 +83,6 @@ StandardCodecSerializer::~StandardCodecSerializer() = default; EncodableValue StandardCodecSerializer::ReadValue( ByteBufferStreamReader* stream) const { EncodedType type = static_cast(stream->ReadByte()); - ; switch (type) { case EncodedType::kNull: return EncodableValue(); @@ -286,7 +285,7 @@ StandardMessageCodec::~StandardMessageCodec() = default; std::unique_ptr StandardMessageCodec::DecodeMessageInternal( const uint8_t* binary_message, - const size_t message_size) const { + size_t message_size) const { StandardCodecSerializer serializer; ByteBufferStreamReader stream(binary_message, message_size); return std::make_unique(serializer.ReadValue(&stream)); @@ -312,7 +311,7 @@ const StandardMethodCodec& StandardMethodCodec::GetInstance() { std::unique_ptr> StandardMethodCodec::DecodeMethodCallInternal(const uint8_t* message, - const size_t message_size) const { + size_t message_size) const { StandardCodecSerializer serializer; ByteBufferStreamReader stream(message, message_size); EncodableValue method_name = serializer.ReadValue(&stream); @@ -380,4 +379,31 @@ StandardMethodCodec::EncodeErrorEnvelopeInternal( return encoded; } +bool StandardMethodCodec::DecodeAndProcessResponseEnvelopeInternal( + const uint8_t* response, + size_t response_size, + MethodResult* result) const { + StandardCodecSerializer serializer; + ByteBufferStreamReader stream(response, response_size); + uint8_t flag = stream.ReadByte(); + switch (flag) { + case 0: { + EncodableValue value = serializer.ReadValue(&stream); + result->Success(value.IsNull() ? nullptr : &value); + return true; + } + case 1: { + EncodableValue code = serializer.ReadValue(&stream); + EncodableValue message = serializer.ReadValue(&stream); + EncodableValue details = serializer.ReadValue(&stream); + result->Error(code.StringValue(), + message.IsNull() ? "" : message.StringValue(), + details.IsNull() ? nullptr : &details); + return true; + } + default: + return false; + } +} + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc index e3f4b15de77a8..4b43b40229038 100644 --- a/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc +++ b/shell/platform/common/cpp/client_wrapper/standard_method_codec_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/standard_method_codec.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" #include "flutter/shell/platform/common/cpp/client_wrapper/testing/encodable_value_utils.h" #include "gtest/gtest.h" @@ -60,7 +61,17 @@ TEST(StandardMethodCodec, HandlesSuccessEnvelopesWithNullResult) { ASSERT_NE(encoded.get(), nullptr); std::vector bytes = {0x00, 0x00}; EXPECT_EQ(*encoded, bytes); - // TODO: Add round-trip check once decoding replies is implemented. + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + [&decoded_successfully](const EncodableValue* result) { + decoded_successfully = true; + EXPECT_EQ(result, nullptr); + }, + nullptr, nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); } TEST(StandardMethodCodec, HandlesSuccessEnvelopesWithResult) { @@ -70,7 +81,17 @@ TEST(StandardMethodCodec, HandlesSuccessEnvelopesWithResult) { ASSERT_NE(encoded.get(), nullptr); std::vector bytes = {0x00, 0x03, 0x2a, 0x00, 0x00, 0x00}; EXPECT_EQ(*encoded, bytes); - // TODO: Add round-trip check once decoding replies is implemented. + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + [&decoded_successfully](const EncodableValue* result) { + decoded_successfully = true; + EXPECT_EQ(result->IntValue(), 42); + }, + nullptr, nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); } TEST(StandardMethodCodec, HandlesErrorEnvelopesWithNulls) { @@ -80,7 +101,22 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithNulls) { std::vector bytes = {0x01, 0x07, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x00, 0x00}; EXPECT_EQ(*encoded, bytes); - // TODO: Add round-trip check once decoding replies is implemented. + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + nullptr, + [&decoded_successfully](const std::string& code, + const std::string& message, + const EncodableValue* details) { + decoded_successfully = true; + EXPECT_EQ(code, "errorCode"); + EXPECT_EQ(message, ""); + EXPECT_EQ(details, nullptr); + }, + nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); } TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { @@ -99,7 +135,24 @@ TEST(StandardMethodCodec, HandlesErrorEnvelopesWithDetails) { 0x0c, 0x02, 0x07, 0x01, 0x61, 0x03, 0x2a, 0x00, 0x00, 0x00, }; EXPECT_EQ(*encoded, bytes); - // TODO: Add round-trip check once decoding replies is implemented. + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + nullptr, + [&decoded_successfully](const std::string& code, + const std::string& message, + const EncodableValue* details) { + decoded_successfully = true; + EXPECT_EQ(code, "errorCode"); + EXPECT_EQ(message, "something failed"); + EXPECT_TRUE(details->IsList()); + EXPECT_EQ(details->ListValue()[0].StringValue(), "a"); + EXPECT_EQ(details->ListValue()[1].IntValue(), 42); + }, + nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); } } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc index 4bd6ddce01a39..580df48645aa9 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.cc @@ -44,6 +44,14 @@ FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger( return reinterpret_cast(1); } +void FlutterDesktopRegistrarSetDestructionHandler( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopOnRegistrarDestroyed callback) { + if (s_stub_implementation) { + s_stub_implementation->RegistrarSetDestructionHandler(callback); + } +} + void FlutterDesktopRegistrarEnableInputBlocking( FlutterDesktopPluginRegistrarRef registrar, const char* channel) { diff --git a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h index c5f9dbbb2a2a5..284d15e974947 100644 --- a/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h +++ b/shell/platform/common/cpp/client_wrapper/testing/stub_flutter_api.h @@ -34,6 +34,10 @@ class StubFlutterApi { virtual ~StubFlutterApi() {} + // Called for FlutterDesktopRegistrarSetDestructionHandler. + virtual void RegistrarSetDestructionHandler( + FlutterDesktopOnRegistrarDestroyed callback) {} + // Called for FlutterDesktopRegistrarEnableInputBlocking. virtual void RegistrarEnableInputBlocking(const char* channel) {} diff --git a/shell/platform/common/cpp/client_wrapper/json_message_codec.cc b/shell/platform/common/cpp/json_message_codec.cc similarity index 53% rename from shell/platform/common/cpp/client_wrapper/json_message_codec.cc rename to shell/platform/common/cpp/json_message_codec.cc index 5610c4762ea83..d2a43bd806ded 100644 --- a/shell/platform/common/cpp/client_wrapper/json_message_codec.cc +++ b/shell/platform/common/cpp/json_message_codec.cc @@ -2,16 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "include/flutter/json_message_codec.h" +#include "flutter/shell/platform/common/cpp/json_message_codec.h" #include #include -#ifdef USE_RAPID_JSON #include "rapidjson/error/en.h" #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" -#endif namespace flutter { @@ -22,8 +20,7 @@ const JsonMessageCodec& JsonMessageCodec::GetInstance() { } std::unique_ptr> JsonMessageCodec::EncodeMessageInternal( - const JsonValueType& message) const { -#ifdef USE_RAPID_JSON + const rapidjson::Document& message) const { // TODO: Look into alternate writers that would avoid the buffer copy. rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -31,37 +28,20 @@ std::unique_ptr> JsonMessageCodec::EncodeMessageInternal( const char* buffer_start = buffer.GetString(); return std::make_unique>( buffer_start, buffer_start + buffer.GetSize()); -#else - Json::StreamWriterBuilder writer_builder; - std::string serialization = Json::writeString(writer_builder, message); - return std::make_unique>(serialization.begin(), - serialization.end()); -#endif } -std::unique_ptr JsonMessageCodec::DecodeMessageInternal( +std::unique_ptr JsonMessageCodec::DecodeMessageInternal( const uint8_t* binary_message, const size_t message_size) const { auto raw_message = reinterpret_cast(binary_message); - auto json_message = std::make_unique(); - std::string parse_errors; - bool parsing_successful = false; -#ifdef USE_RAPID_JSON + auto json_message = std::make_unique(); rapidjson::ParseResult result = json_message->Parse(raw_message, message_size); - parsing_successful = result == rapidjson::ParseErrorCode::kParseErrorNone; - if (!parsing_successful) { - parse_errors = rapidjson::GetParseError_En(result.Code()); - } -#else - Json::CharReaderBuilder reader_builder; - std::unique_ptr parser(reader_builder.newCharReader()); - parsing_successful = parser->parse(raw_message, raw_message + message_size, - json_message.get(), &parse_errors); -#endif + bool parsing_successful = + result == rapidjson::ParseErrorCode::kParseErrorNone; if (!parsing_successful) { std::cerr << "Unable to parse JSON message:" << std::endl - << parse_errors << std::endl; + << rapidjson::GetParseError_En(result.Code()) << std::endl; return nullptr; } return json_message; diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h b/shell/platform/common/cpp/json_message_codec.h similarity index 64% rename from shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h rename to shell/platform/common/cpp/json_message_codec.h index ba1aa2ffc3520..81f2502c0c71d 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h +++ b/shell/platform/common/cpp/json_message_codec.h @@ -2,17 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_MESSAGE_CODEC_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_MESSAGE_CODEC_H_ +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_MESSAGE_CODEC_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_MESSAGE_CODEC_H_ -#include "json_type.h" -#include "message_codec.h" +#include + +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/message_codec.h" namespace flutter { // A message encoding/decoding mechanism for communications to/from the // Flutter engine via JSON channels. -class JsonMessageCodec : public MessageCodec { +class JsonMessageCodec : public MessageCodec { public: // Returns the shared instance of the codec. static const JsonMessageCodec& GetInstance(); @@ -28,15 +29,15 @@ class JsonMessageCodec : public MessageCodec { JsonMessageCodec() = default; // |flutter::MessageCodec| - std::unique_ptr DecodeMessageInternal( + std::unique_ptr DecodeMessageInternal( const uint8_t* binary_message, const size_t message_size) const override; // |flutter::MessageCodec| std::unique_ptr> EncodeMessageInternal( - const JsonValueType& message) const override; + const rapidjson::Document& message) const override; }; } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_MESSAGE_CODEC_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_MESSAGE_CODEC_H_ diff --git a/shell/platform/common/cpp/json_message_codec_unittests.cc b/shell/platform/common/cpp/json_message_codec_unittests.cc new file mode 100644 index 0000000000000..3354f30a728df --- /dev/null +++ b/shell/platform/common/cpp/json_message_codec_unittests.cc @@ -0,0 +1,46 @@ +// 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/shell/platform/common/cpp/json_message_codec.h" + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +// Validates round-trip encoding and decoding of |value|. +static void CheckEncodeDecode(const rapidjson::Document& value) { + const JsonMessageCodec& codec = JsonMessageCodec::GetInstance(); + auto encoded = codec.EncodeMessage(value); + ASSERT_TRUE(encoded); + auto decoded = codec.DecodeMessage(*encoded); + EXPECT_EQ(value, *decoded); +} + +} // namespace + +// Tests that a JSON document with various data types round-trips correctly. +TEST(JsonMessageCodec, EncodeDecode) { + rapidjson::Document array(rapidjson::kArrayType); + auto& allocator = array.GetAllocator(); + + array.PushBack("string", allocator); + + rapidjson::Value map(rapidjson::kObjectType); + map.AddMember("a", -7, allocator); + map.AddMember("b", std::numeric_limits::max(), allocator); + map.AddMember("c", 3.14159, allocator); + map.AddMember("d", true, allocator); + map.AddMember("e", rapidjson::Value(), allocator); + array.PushBack(map, allocator); + + CheckEncodeDecode(array); +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/json_method_codec.cc b/shell/platform/common/cpp/json_method_codec.cc similarity index 57% rename from shell/platform/common/cpp/client_wrapper/json_method_codec.cc rename to shell/platform/common/cpp/json_method_codec.cc index 6c9528ff2e304..4a37eb4950d33 100644 --- a/shell/platform/common/cpp/client_wrapper/json_method_codec.cc +++ b/shell/platform/common/cpp/json_method_codec.cc @@ -2,16 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "include/flutter/json_method_codec.h" +#include "flutter/shell/platform/common/cpp/json_method_codec.h" -#include "include/flutter/json_message_codec.h" +#include "flutter/shell/platform/common/cpp/json_message_codec.h" namespace flutter { namespace { + // Keys used in MethodCall encoding. constexpr char kMessageMethodKey[] = "method"; constexpr char kMessageArgumentsKey[] = "args"; + +// Returns a new document containing only |element|, which must be an element +// in |document|. This is a move rather than a copy, so it is efficient but +// destructive to the data in |document|. +std::unique_ptr ExtractElement( + rapidjson::Document* document, + rapidjson::Value* subtree) { + auto extracted = std::make_unique(); + // Pull the subtree up to the root of the document. + document->Swap(*subtree); + // Swap the entire document into |extracted|. Unlike the swap above this moves + // the allocator ownership, so the data won't be deleted when |document| is + // destroyed. + extracted->Swap(*document); + return extracted; +} + } // namespace // static @@ -20,16 +38,15 @@ const JsonMethodCodec& JsonMethodCodec::GetInstance() { return sInstance; } -std::unique_ptr> +std::unique_ptr> JsonMethodCodec::DecodeMethodCallInternal(const uint8_t* message, - const size_t message_size) const { - std::unique_ptr json_message = + size_t message_size) const { + std::unique_ptr json_message = JsonMessageCodec::GetInstance().DecodeMessage(message, message_size); if (!json_message) { return nullptr; } -#if USE_RAPID_JSON auto method_name_iter = json_message->FindMember(kMessageMethodKey); if (method_name_iter == json_message->MemberEnd()) { return nullptr; @@ -41,36 +58,14 @@ JsonMethodCodec::DecodeMethodCallInternal(const uint8_t* message, auto arguments_iter = json_message->FindMember(kMessageArgumentsKey); std::unique_ptr arguments; if (arguments_iter != json_message->MemberEnd()) { - // Pull the arguments subtree up to the root of json_message. This is - // destructive to json_message, but the full value is no longer needed, and - // this avoids a subtree copy. - // Note: The static_cast is for compatibility with RapidJSON 1.1; master - // already allows swapping a Document with a Value directly. Once there is - // a new RapidJSON release (at which point clients can be expected to have - // that change in the version they depend on) remove the cast. - static_cast(json_message.get()) - ->Swap(arguments_iter->value); - // Swap it into |arguments|. This moves the allocator ownership, so that - // the data won't be deleted when json_message goes out of scope. - arguments = std::make_unique(); - arguments->Swap(*json_message); + arguments = ExtractElement(json_message.get(), &(arguments_iter->value)); } return std::make_unique>( method_name, std::move(arguments)); -#else - Json::Value method = (*json_message)[kMessageMethodKey]; - if (method.isNull()) { - return nullptr; - } - return std::make_unique>( - method.asString(), - std::make_unique((*json_message)[kMessageArgumentsKey])); -#endif } std::unique_ptr> JsonMethodCodec::EncodeMethodCallInternal( - const MethodCall& method_call) const { -#if USE_RAPID_JSON + const MethodCall& method_call) const { // TODO: Consider revisiting the codec APIs to avoid the need to copy // everything when doing encoding (e.g., by having a version that takes // owership of the object to encode, so that it can be moved instead). @@ -83,20 +78,13 @@ std::unique_ptr> JsonMethodCodec::EncodeMethodCallInternal( } message.AddMember(kMessageMethodKey, name, allocator); message.AddMember(kMessageArgumentsKey, arguments, allocator); -#else - Json::Value message(Json::objectValue); - message[kMessageMethodKey] = method_call.method_name(); - const Json::Value* arguments = method_call.arguments(); - message[kMessageArgumentsKey] = arguments ? *arguments : Json::Value(); -#endif return JsonMessageCodec::GetInstance().EncodeMessage(message); } std::unique_ptr> JsonMethodCodec::EncodeSuccessEnvelopeInternal( - const JsonValueType* result) const { -#if USE_RAPID_JSON + const rapidjson::Document* result) const { rapidjson::Document envelope; envelope.SetArray(); rapidjson::Value result_value; @@ -104,10 +92,6 @@ JsonMethodCodec::EncodeSuccessEnvelopeInternal( result_value.CopyFrom(*result, envelope.GetAllocator()); } envelope.PushBack(result_value, envelope.GetAllocator()); -#else - Json::Value envelope(Json::arrayValue); - envelope.append(result == nullptr ? Json::Value() : *result); -#endif return JsonMessageCodec::GetInstance().EncodeMessage(envelope); } @@ -116,8 +100,7 @@ std::unique_ptr> JsonMethodCodec::EncodeErrorEnvelopeInternal( const std::string& error_code, const std::string& error_message, - const JsonValueType* error_details) const { -#if USE_RAPID_JSON + const rapidjson::Document* error_details) const { rapidjson::Document envelope(rapidjson::kArrayType); auto& allocator = envelope.GetAllocator(); envelope.PushBack(rapidjson::Value(error_code, allocator), allocator); @@ -127,14 +110,40 @@ JsonMethodCodec::EncodeErrorEnvelopeInternal( details_value.CopyFrom(*error_details, allocator); } envelope.PushBack(details_value, allocator); -#else - Json::Value envelope(Json::arrayValue); - envelope.append(error_code); - envelope.append(error_message.empty() ? Json::Value() : error_message); - envelope.append(error_details == nullptr ? Json::Value() : *error_details); -#endif return JsonMessageCodec::GetInstance().EncodeMessage(envelope); } +bool JsonMethodCodec::DecodeAndProcessResponseEnvelopeInternal( + const uint8_t* response, + size_t response_size, + MethodResult* result) const { + std::unique_ptr json_response = + JsonMessageCodec::GetInstance().DecodeMessage(response, response_size); + if (!json_response) { + return false; + } + if (!json_response->IsArray()) { + return false; + } + switch (json_response->Size()) { + case 1: { + std::unique_ptr value = + ExtractElement(json_response.get(), &((*json_response)[0])); + result->Success(value->IsNull() ? nullptr : value.get()); + return true; + } + case 3: { + std::string code = (*json_response)[0].GetString(); + std::string message = (*json_response)[1].GetString(); + std::unique_ptr details = + ExtractElement(json_response.get(), &((*json_response)[2])); + result->Error(code, message, details->IsNull() ? nullptr : details.get()); + return true; + } + default: + return false; + } +} + } // namespace flutter diff --git a/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h b/shell/platform/common/cpp/json_method_codec.h similarity index 54% rename from shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h rename to shell/platform/common/cpp/json_method_codec.h index 92af82000ac96..25e3963fcb03d 100644 --- a/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h +++ b/shell/platform/common/cpp/json_method_codec.h @@ -2,17 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_METHOD_CODEC_H_ -#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_METHOD_CODEC_H_ +#ifndef FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_METHOD_CODEC_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_METHOD_CODEC_H_ -#include "json_type.h" -#include "method_call.h" -#include "method_codec.h" +#include + +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_call.h" +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_codec.h" namespace flutter { // An implementation of MethodCodec that uses JSON strings as the serialization. -class JsonMethodCodec : public MethodCodec { +class JsonMethodCodec : public MethodCodec { public: // Returns the shared instance of the codec. static const JsonMethodCodec& GetInstance(); @@ -28,25 +29,31 @@ class JsonMethodCodec : public MethodCodec { JsonMethodCodec() = default; // |flutter::MethodCodec| - std::unique_ptr> DecodeMethodCallInternal( + std::unique_ptr> DecodeMethodCallInternal( const uint8_t* message, const size_t message_size) const override; // |flutter::MethodCodec| std::unique_ptr> EncodeMethodCallInternal( - const MethodCall& method_call) const override; + const MethodCall& method_call) const override; // |flutter::MethodCodec| std::unique_ptr> EncodeSuccessEnvelopeInternal( - const JsonValueType* result) const override; + const rapidjson::Document* result) const override; // |flutter::MethodCodec| std::unique_ptr> EncodeErrorEnvelopeInternal( const std::string& error_code, const std::string& error_message, - const JsonValueType* error_details) const override; + const rapidjson::Document* error_details) const override; + + // |flutter::MethodCodec| + bool DecodeAndProcessResponseEnvelopeInternal( + const uint8_t* response, + const size_t response_size, + MethodResult* result) const override; }; } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_CLIENT_WRAPPER_INCLUDE_FLUTTER_JSON_METHOD_CODEC_H_ +#endif // FLUTTER_SHELL_PLATFORM_COMMON_CPP_JSON_METHOD_CODEC_H_ diff --git a/shell/platform/common/cpp/json_method_codec_unittests.cc b/shell/platform/common/cpp/json_method_codec_unittests.cc new file mode 100644 index 0000000000000..f9d2ec882fc45 --- /dev/null +++ b/shell/platform/common/cpp/json_method_codec_unittests.cc @@ -0,0 +1,160 @@ +// 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/shell/platform/common/cpp/json_method_codec.h" + +#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/method_result_functions.h" +#include "gtest/gtest.h" + +namespace flutter { + +namespace { + +// Returns true if the given method calls have the same method name, and their +// arguments have equivalent values. +bool MethodCallsAreEqual(const MethodCall& a, + const MethodCall& b) { + if (a.method_name() != b.method_name()) { + return false; + } + // Treat nullptr and Null as equivalent. + if ((!a.arguments() || a.arguments()->IsNull()) && + (!b.arguments() || b.arguments()->IsNull())) { + return true; + } + return *a.arguments() == *b.arguments(); +} + +} // namespace + +TEST(JsonMethodCodec, HandlesMethodCallsWithNullArguments) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + MethodCall call("hello", nullptr); + auto encoded = codec.EncodeMethodCall(call); + ASSERT_TRUE(encoded); + std::unique_ptr> decoded = + codec.DecodeMethodCall(*encoded); + ASSERT_TRUE(decoded); + EXPECT_TRUE(MethodCallsAreEqual(call, *decoded)); +} + +TEST(JsonMethodCodec, HandlesMethodCallsWithArgument) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + + auto arguments = std::make_unique(rapidjson::kArrayType); + auto& allocator = arguments->GetAllocator(); + arguments->PushBack(42, allocator); + arguments->PushBack("world", allocator); + MethodCall call("hello", std::move(arguments)); + auto encoded = codec.EncodeMethodCall(call); + ASSERT_TRUE(encoded); + std::unique_ptr> decoded = + codec.DecodeMethodCall(*encoded); + ASSERT_TRUE(decoded); + EXPECT_TRUE(MethodCallsAreEqual(call, *decoded)); +} + +TEST(JsonMethodCodec, HandlesSuccessEnvelopesWithNullResult) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + auto encoded = codec.EncodeSuccessEnvelope(); + ASSERT_TRUE(encoded); + std::vector bytes = {'[', 'n', 'u', 'l', 'l', ']'}; + EXPECT_EQ(*encoded, bytes); + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + [&decoded_successfully](const rapidjson::Document* result) { + decoded_successfully = true; + EXPECT_EQ(result, nullptr); + }, + nullptr, nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); +} + +TEST(JsonMethodCodec, HandlesSuccessEnvelopesWithResult) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + rapidjson::Document result; + result.SetInt(42); + auto encoded = codec.EncodeSuccessEnvelope(&result); + ASSERT_TRUE(encoded); + std::vector bytes = {'[', '4', '2', ']'}; + EXPECT_EQ(*encoded, bytes); + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + [&decoded_successfully](const rapidjson::Document* result) { + decoded_successfully = true; + EXPECT_EQ(result->GetInt(), 42); + }, + nullptr, nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); +} + +TEST(JsonMethodCodec, HandlesErrorEnvelopesWithNulls) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + auto encoded = codec.EncodeErrorEnvelope("errorCode"); + ASSERT_TRUE(encoded); + std::vector bytes = { + '[', '"', 'e', 'r', 'r', 'o', 'r', 'C', 'o', 'd', 'e', + '"', ',', '"', '"', ',', 'n', 'u', 'l', 'l', ']', + }; + EXPECT_EQ(*encoded, bytes); + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + nullptr, + [&decoded_successfully](const std::string& code, + const std::string& message, + const rapidjson::Document* details) { + decoded_successfully = true; + EXPECT_EQ(code, "errorCode"); + EXPECT_EQ(message, ""); + EXPECT_EQ(details, nullptr); + }, + nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); +} + +TEST(JsonMethodCodec, HandlesErrorEnvelopesWithDetails) { + const JsonMethodCodec& codec = JsonMethodCodec::GetInstance(); + rapidjson::Document details(rapidjson::kArrayType); + auto& allocator = details.GetAllocator(); + details.PushBack("a", allocator); + details.PushBack(42, allocator); + auto encoded = + codec.EncodeErrorEnvelope("errorCode", "something failed", &details); + ASSERT_NE(encoded.get(), nullptr); + std::vector bytes = { + '[', '"', 'e', 'r', 'r', 'o', 'r', 'C', 'o', 'd', 'e', '"', ',', '"', + 's', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', ' ', 'f', 'a', 'i', 'l', + 'e', 'd', '"', ',', '[', '"', 'a', '"', ',', '4', '2', ']', ']', + }; + EXPECT_EQ(*encoded, bytes); + + bool decoded_successfully = false; + MethodResultFunctions result_handler( + nullptr, + [&decoded_successfully](const std::string& code, + const std::string& message, + const rapidjson::Document* details) { + decoded_successfully = true; + EXPECT_EQ(code, "errorCode"); + EXPECT_EQ(message, "something failed"); + EXPECT_TRUE(details->IsArray()); + EXPECT_EQ(std::string((*details)[0].GetString()), "a"); + EXPECT_EQ((*details)[1].GetInt(), 42); + }, + nullptr); + codec.DecodeAndProcessResponseEnvelope(encoded->data(), encoded->size(), + &result_handler); + EXPECT_TRUE(decoded_successfully); +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/path_utils.cc b/shell/platform/common/cpp/path_utils.cc new file mode 100644 index 0000000000000..6da31077a761e --- /dev/null +++ b/shell/platform/common/cpp/path_utils.cc @@ -0,0 +1,37 @@ +// 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/shell/platform/common/cpp/path_utils.h" + +#if defined(_WIN32) +#include +#elif defined(__linux__) +#include +#include +#endif + +namespace flutter { + +std::filesystem::path GetExecutableDirectory() { +#if defined(_WIN32) + wchar_t buffer[MAX_PATH]; + if (GetModuleFileName(nullptr, buffer, MAX_PATH) == 0) { + return std::filesystem::path(); + } + std::filesystem::path executable_path(buffer); + return executable_path.remove_filename(); +#elif defined(__linux__) + char buffer[PATH_MAX + 1]; + ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer)); + if (length > PATH_MAX) { + return std::filesystem::path(); + } + std::filesystem::path executable_path(std::string(buffer, length)); + return executable_path.remove_filename(); +#else + return std::filesystem::path(); +#endif +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/path_utils.h b/shell/platform/common/cpp/path_utils.h new file mode 100644 index 0000000000000..62cd3fd840a9e --- /dev/null +++ b/shell/platform/common/cpp/path_utils.h @@ -0,0 +1,18 @@ +// 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_SHELL_PLATFORM_GLFW_PATH_UTILS_H_ +#define FLUTTER_SHELL_PLATFORM_GLFW_PATH_UTILS_H_ + +#include + +namespace flutter { + +// Returns the path of the directory containing this executable, or an empty +// path if the directory cannot be found. +std::filesystem::path GetExecutableDirectory(); + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_GLFW_PATH_UTILS_H_ diff --git a/shell/platform/common/cpp/path_utils_unittests.cc b/shell/platform/common/cpp/path_utils_unittests.cc new file mode 100644 index 0000000000000..2044d34662bc4 --- /dev/null +++ b/shell/platform/common/cpp/path_utils_unittests.cc @@ -0,0 +1,24 @@ +// 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/shell/platform/common/cpp/path_utils.h" + +#include "gtest/gtest.h" + +namespace flutter { + +// Tests that GetExecutableDirectory returns a valid, absolute path. +TEST(PathUtilsTest, ExecutableDirector) { + std::filesystem::path exe_directory = GetExecutableDirectory(); +#if defined(__linux__) || defined(_WIN32) + EXPECT_EQ(exe_directory.empty(), false); + EXPECT_EQ(exe_directory.is_absolute(), true); +#else + // On platforms where it's not implemented, it should indicate that + // by returning an empty path. + EXPECT_EQ(exe_directory.empty(), true); +#endif +} + +} // namespace flutter diff --git a/shell/platform/common/cpp/public/flutter_plugin_registrar.h b/shell/platform/common/cpp/public/flutter_plugin_registrar.h index 1caa3cee1f9e4..95f0abf139608 100644 --- a/shell/platform/common/cpp/public/flutter_plugin_registrar.h +++ b/shell/platform/common/cpp/public/flutter_plugin_registrar.h @@ -18,10 +18,19 @@ extern "C" { // Opaque reference to a plugin registrar. typedef struct FlutterDesktopPluginRegistrar* FlutterDesktopPluginRegistrarRef; +// Function pointer type for registrar destruction callback. +typedef void (*FlutterDesktopOnRegistrarDestroyed)( + FlutterDesktopPluginRegistrarRef); + // Returns the engine messenger associated with this registrar. FLUTTER_EXPORT FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger(FlutterDesktopPluginRegistrarRef registrar); +// Registers a callback to be called when the plugin registrar is destroyed. +FLUTTER_EXPORT void FlutterDesktopRegistrarSetDestructionHandler( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopOnRegistrarDestroyed callback); + // Enables input blocking on the given channel. // // If set, then the Flutter window will disable input callbacks diff --git a/shell/platform/darwin/BUILD.gn b/shell/platform/darwin/BUILD.gn index f5271ca366e60..bfc29dfe79d76 100644 --- a/shell/platform/darwin/BUILD.gn +++ b/shell/platform/darwin/BUILD.gn @@ -20,6 +20,9 @@ group("darwin") { } source_set("flutter_channels") { + cflags_objc = flutter_cflags_objc + cflags_objcc = flutter_cflags_objcc + sources = [ "common/buffer_conversions.h", "common/buffer_conversions.mm", diff --git a/shell/platform/darwin/common/BUILD.gn b/shell/platform/darwin/common/BUILD.gn index 8c5663e84c415..45a9e3092b2bc 100644 --- a/shell/platform/darwin/common/BUILD.gn +++ b/shell/platform/darwin/common/BUILD.gn @@ -2,9 +2,13 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//flutter/common/config.gni") import("framework_shared.gni") source_set("common") { + cflags_objc = flutter_cflags_objc + cflags_objcc = flutter_cflags_objcc + sources = [ "buffer_conversions.h", "buffer_conversions.mm", @@ -34,6 +38,9 @@ config("framework_relative_headers") { # Framework code shared between iOS and macOS. source_set("framework_shared") { + cflags_objc = flutter_cflags_objc + cflags_objcc = flutter_cflags_objcc + sources = [ "framework/Source/FlutterChannels.mm", "framework/Source/FlutterCodecs.mm", diff --git a/shell/platform/darwin/common/framework/Headers/FlutterTexture.h b/shell/platform/darwin/common/framework/Headers/FlutterTexture.h index 962972025e2f4..7c3435c834f49 100644 --- a/shell/platform/darwin/common/framework/Headers/FlutterTexture.h +++ b/shell/platform/darwin/common/framework/Headers/FlutterTexture.h @@ -25,7 +25,7 @@ FLUTTER_EXPORT /** * Called when the texture is unregistered. * - * Called on the GPU thread. + * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject*)texture; @@ -45,7 +45,7 @@ FLUTTER_EXPORT /** * Notifies Flutter that the content of the previously registered texture has been updated. * - * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the GPU thread. + * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** diff --git a/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm b/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm index b06dab7b85577..c56fca266abb9 100644 --- a/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm +++ b/shell/platform/darwin/common/framework/Source/FlutterCodecs.mm @@ -13,7 +13,8 @@ + (instancetype)sharedInstance { return _sharedInstance; } -- (NSData*)encode:(NSData*)message { +- (NSData*)encode:(id)message { + NSAssert(!message || [message isKindOfClass:[NSData class]], @""); return message; } @@ -31,10 +32,12 @@ + (instancetype)sharedInstance { return _sharedInstance; } -- (NSData*)encode:(NSString*)message { +- (NSData*)encode:(id)message { if (message == nil) return nil; - const char* utf8 = message.UTF8String; + NSAssert([message isKindOfClass:[NSString class]], @""); + NSString* stringMessage = message; + const char* utf8 = stringMessage.UTF8String; return [NSData dataWithBytes:utf8 length:strlen(utf8)]; } diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 1671975c8d0e5..8fd88147c7e64 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -37,14 +37,14 @@ _flutter_framework_headers = [ _flutter_framework_headers_copy_dir = "$_flutter_framework_dir/Headers" -shared_library("create_flutter_framework_dylib") { - visibility = [ ":*" ] +# TODO(54502): move this variable into //build/config/ios/ios_sdk.gni +# Version of iOS that we're targeting for tests. +ios_testing_deployment_target = "13.0" - output_name = "Flutter" - - ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ] - - public = _flutter_framework_headers +source_set("flutter_framework_source") { + visibility = [ ":*" ] + cflags_objc = flutter_cflags_objc + cflags_objcc = flutter_cflags_objcc sources = [ "framework/Source/FlutterAppDelegate.mm", @@ -73,6 +73,8 @@ shared_library("create_flutter_framework_dylib") { "framework/Source/FlutterView.mm", "framework/Source/FlutterViewController.mm", "framework/Source/FlutterViewController_Internal.h", + "framework/Source/SemanticsObject.h", + "framework/Source/SemanticsObject.mm", "framework/Source/accessibility_bridge.h", "framework/Source/accessibility_bridge.mm", "framework/Source/accessibility_text_entry.h", @@ -107,6 +109,21 @@ shared_library("create_flutter_framework_dylib") { sources += _flutter_framework_headers + defines = [ "FLUTTER_FRAMEWORK=1" ] + + if (shell_enable_metal) { + defines += [ "FLUTTER_SHELL_ENABLE_METAL=1" ] + + sources += [ + "ios_context_metal.h", + "ios_context_metal.mm", + "ios_external_texture_metal.h", + "ios_external_texture_metal.mm", + "ios_surface_metal.h", + "ios_surface_metal.mm", + ] + } + deps = [ ":ios_gpu_configuration", "//flutter/common", @@ -123,19 +140,6 @@ shared_library("create_flutter_framework_dylib") { public_configs = [ "//flutter:config" ] - defines = [ "FLUTTER_FRAMEWORK=1" ] - - if (shell_enable_metal) { - defines += [ "FLUTTER_SHELL_ENABLE_METAL=1" ] - - sources += [ - "ios_context_metal.h", - "ios_context_metal.mm", - "ios_surface_metal.h", - "ios_surface_metal.mm", - ] - } - libs = [ "CoreMedia.framework", "CoreVideo.framework", @@ -146,6 +150,146 @@ shared_library("create_flutter_framework_dylib") { ] } +ocmock_path = "../../../../../third_party/ocmock/Source" + +# TODO(54503): Clone the OCMock repository so we can add a BUILD.gn to it. +static_library("ocmock") { + configs -= [ "//build/config/compiler:chromium_code" ] + cflags = [ + "-fvisibility=default", + "-mios-simulator-version-min=$ios_testing_deployment_target", + "-Wno-misleading-indentation", + ] + sources = [ + "$ocmock_path/OCMock/NSInvocation+OCMAdditions.h", + "$ocmock_path/OCMock/NSInvocation+OCMAdditions.m", + "$ocmock_path/OCMock/NSMethodSignature+OCMAdditions.h", + "$ocmock_path/OCMock/NSMethodSignature+OCMAdditions.m", + "$ocmock_path/OCMock/NSNotificationCenter+OCMAdditions.h", + "$ocmock_path/OCMock/NSNotificationCenter+OCMAdditions.m", + "$ocmock_path/OCMock/NSObject+OCMAdditions.h", + "$ocmock_path/OCMock/NSObject+OCMAdditions.m", + "$ocmock_path/OCMock/NSValue+OCMAdditions.h", + "$ocmock_path/OCMock/NSValue+OCMAdditions.m", + "$ocmock_path/OCMock/OCClassMockObject.h", + "$ocmock_path/OCMock/OCClassMockObject.m", + "$ocmock_path/OCMock/OCMArg.h", + "$ocmock_path/OCMock/OCMArg.m", + "$ocmock_path/OCMock/OCMArgAction.h", + "$ocmock_path/OCMock/OCMArgAction.m", + "$ocmock_path/OCMock/OCMBlockArgCaller.h", + "$ocmock_path/OCMock/OCMBlockArgCaller.m", + "$ocmock_path/OCMock/OCMBlockCaller.h", + "$ocmock_path/OCMock/OCMBlockCaller.m", + "$ocmock_path/OCMock/OCMBoxedReturnValueProvider.h", + "$ocmock_path/OCMock/OCMBoxedReturnValueProvider.m", + "$ocmock_path/OCMock/OCMConstraint.h", + "$ocmock_path/OCMock/OCMConstraint.m", + "$ocmock_path/OCMock/OCMExceptionReturnValueProvider.h", + "$ocmock_path/OCMock/OCMExceptionReturnValueProvider.m", + "$ocmock_path/OCMock/OCMExpectationRecorder.h", + "$ocmock_path/OCMock/OCMExpectationRecorder.m", + "$ocmock_path/OCMock/OCMFunctions.h", + "$ocmock_path/OCMock/OCMFunctions.m", + "$ocmock_path/OCMock/OCMFunctionsPrivate.h", + "$ocmock_path/OCMock/OCMIndirectReturnValueProvider.h", + "$ocmock_path/OCMock/OCMIndirectReturnValueProvider.m", + "$ocmock_path/OCMock/OCMInvocationExpectation.h", + "$ocmock_path/OCMock/OCMInvocationExpectation.m", + "$ocmock_path/OCMock/OCMInvocationMatcher.h", + "$ocmock_path/OCMock/OCMInvocationMatcher.m", + "$ocmock_path/OCMock/OCMInvocationStub.h", + "$ocmock_path/OCMock/OCMInvocationStub.m", + "$ocmock_path/OCMock/OCMLocation.h", + "$ocmock_path/OCMock/OCMLocation.m", + "$ocmock_path/OCMock/OCMMacroState.h", + "$ocmock_path/OCMock/OCMMacroState.m", + "$ocmock_path/OCMock/OCMNotificationPoster.h", + "$ocmock_path/OCMock/OCMNotificationPoster.m", + "$ocmock_path/OCMock/OCMObserverRecorder.h", + "$ocmock_path/OCMock/OCMObserverRecorder.m", + "$ocmock_path/OCMock/OCMPassByRefSetter.h", + "$ocmock_path/OCMock/OCMPassByRefSetter.m", + "$ocmock_path/OCMock/OCMRealObjectForwarder.h", + "$ocmock_path/OCMock/OCMRealObjectForwarder.m", + "$ocmock_path/OCMock/OCMRecorder.h", + "$ocmock_path/OCMock/OCMRecorder.m", + "$ocmock_path/OCMock/OCMReturnValueProvider.h", + "$ocmock_path/OCMock/OCMReturnValueProvider.m", + "$ocmock_path/OCMock/OCMStubRecorder.h", + "$ocmock_path/OCMock/OCMStubRecorder.m", + "$ocmock_path/OCMock/OCMVerifier.h", + "$ocmock_path/OCMock/OCMVerifier.m", + "$ocmock_path/OCMock/OCMock.h", + "$ocmock_path/OCMock/OCMockObject.h", + "$ocmock_path/OCMock/OCMockObject.m", + "$ocmock_path/OCMock/OCObserverMockObject.h", + "$ocmock_path/OCMock/OCObserverMockObject.m", + "$ocmock_path/OCMock/OCPartialMockObject.h", + "$ocmock_path/OCMock/OCPartialMockObject.m", + "$ocmock_path/OCMock/OCProtocolMockObject.h", + "$ocmock_path/OCMock/OCProtocolMockObject.m", + ] + include_dirs = [ "$ocmock_path" ] +} + +ios_test_flutter_path = rebase_path("$root_out_dir/libios_test_flutter.dylib") +platform_frameworks_path = "$ios_sdk_path/../../Library/Frameworks/" + +# NOTE: This currently only supports simulator targets because of the install_name. +# TODO(54504): Switch the install_name and make the test runner copy the dynamic +# library into the testing bundle. +shared_library("ios_test_flutter") { + visibility = [ ":*" ] + cflags = [ + "-fvisibility=default", + "-F$platform_frameworks_path", + "-fobjc-arc", + "-mios-simulator-version-min=$ios_testing_deployment_target", + ] + ldflags = [ + "-F$platform_frameworks_path", + "-Wl,-framework,XCTest", + "-Wl,-install_name,$ios_test_flutter_path", + ] + configs -= [ + "//build/config/gcc:symbol_visibility_hidden", + "//build/config:symbol_visibility_hidden", + ] + sources = [ + "framework/Source/FlutterBinaryMessengerRelayTest.mm", + "framework/Source/FlutterEngineTest.mm", + "framework/Source/FlutterPluginAppLifeCycleDelegateTest.m", + "framework/Source/FlutterTextInputPluginTest.m", + "framework/Source/FlutterViewControllerTest.mm", + "framework/Source/SemanticsObjectTest.mm", + ] + deps = [ + ":flutter_framework_source", + ":ocmock", + "//flutter/shell/platform/darwin/common:framework_shared", + "//third_party/skia", + ] + include_dirs = [ "$ocmock_path" ] + public_configs = [ "//flutter:config" ] +} + +shared_library("create_flutter_framework_dylib") { + visibility = [ ":*" ] + + output_name = "Flutter" + + ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ] + + public = _flutter_framework_headers + + deps = [ + ":flutter_framework_source", + ] + + public_configs = [ "//flutter:config" ] +} + copy("copy_dylib") { visibility = [ ":*" ] @@ -250,6 +394,7 @@ test_fixtures("flutter_tests_fixtures") { fixtures = [] } +# Note: This currently isn't used, it might be removed. ios_app("FlutterTests") { testonly = true diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 51782ceca07cb..46980d609a078 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -36,6 +36,11 @@ extern NSString* const FlutterDefaultDartEntrypoint; * `FlutterViewController` instances to maintain state and/or asynchronous tasks * (such as downloading a large file). * + * A FlutterEngine can also be used to prewarm the Dart execution environment and reduce the + * latency of showing the Flutter screen when a `FlutterViewController` is created and presented. + * See http://flutter.dev/docs/development/add-to-app/performance for more details on loading + * performance. + * * Alternatively, you can simply create a new `FlutterViewController` with only a * `FlutterDartProject`. That `FlutterViewController` will internally manage its * own instance of a FlutterEngine, but will not guarantee survival of the engine diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h b/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h index 24acbc08ad6fe..1994cfc81f954 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h @@ -22,7 +22,7 @@ typedef void (^FlutterHeadlessDartRunnerCallback)(BOOL success); /** - * The FlutterHeadlessDartRunner runs Flutter Dart code with a null rasterizer, + * The deprecated FlutterHeadlessDartRunner runs Flutter Dart code with a null rasterizer, * and no native drawing surface. It is appropriate for use in running Dart * code e.g. in the background from a plugin. * diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h b/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h index 72086dec13a83..ba22e62b33394 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h @@ -31,7 +31,7 @@ FLUTTER_EXPORT * * The implementation of this method should create a new `UIView` and return it. * - * @param frame The rectangle for the newly created `UIView` measued in points. + * @param frame The rectangle for the newly created `UIView` measured in points. * @param viewId A unique identifier for this `UIView`. * @param args Parameters for creating the `UIView` sent from the Dart side of the Flutter app. * If `createArgsCodec` is not implemented, or if no creation arguments were sent from the Dart diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index 81d03d7ea6ac7..06d20498a0763 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h @@ -398,7 +398,7 @@ typedef enum { * * @param pluginKey The unique key identifying the plugin. */ -- (NSObject*)registrarForPlugin:(NSString*)pluginKey; +- (nullable NSObject*)registrarForPlugin:(NSString*)pluginKey; /** * Returns whether the specified plugin has been registered. * diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 9753cdb68bca0..77bf219f128a0 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -35,14 +35,17 @@ extern NSNotificationName const FlutterSemanticsUpdateNotification; * handled by `FlutterEngine`. Calls on this class to those members all proxy through to the * `FlutterEngine` attached FlutterViewController. * - * A FlutterViewController can be initialized either with an already-running `FlutterEngine` via - * the `initWithEngine:` initializer, or it can be initialized with a `FlutterDartProject` that - * will be used to implicitly spin up a new `FlutterEngine`. Creating a `FlutterEngine before - * showing a `FlutterViewController` can be used to pre-initialize the Dart VM and to prepare the - * isolate in order to reduce the latency to the first rendered frame. Holding a `FlutterEngine` - * independently of FlutterViewControllers can also be used to not to lose Dart-related state and - * asynchronous tasks when navigating back and forth between a FlutterViewController and other - * `UIViewController`s. + * A FlutterViewController can be initialized either with an already-running `FlutterEngine` via the + * `initWithEngine:` initializer, or it can be initialized with a `FlutterDartProject` that will be + * used to implicitly spin up a new `FlutterEngine`. Creating a `FlutterEngine before showing a + * FlutterViewController can be used to pre-initialize the Dart VM and to prepare the isolate in + * order to reduce the latency to the first rendered frame. See + * https://flutter.dev/docs/development/add-to-app/performance for more details on loading + * latency. + * + * Holding a `FlutterEngine` independently of FlutterViewControllers can also be used to not to lose + * Dart-related state and asynchronous tasks when navigating back and forth between a + * FlutterViewController and other `UIViewController`s. */ FLUTTER_EXPORT @interface FlutterViewController : UIViewController @@ -64,6 +67,9 @@ FLUTTER_EXPORT * Initializes a new FlutterViewController and `FlutterEngine` with the specified * `FlutterDartProject`. * + * This will implicitly create a new `FlutterEngine` which is retrievable via the `engine` property + * after initialization. + * * @param project The `FlutterDartProject` to initialize the `FlutterEngine` with. * @param nibName The NIB name to initialize this UIViewController with. * @param nibBundle The NIB bundle. @@ -174,7 +180,9 @@ FLUTTER_EXPORT @property(nonatomic, getter=isViewOpaque) BOOL viewOpaque; /** - * The `FlutterEngine` instance for this view controller. + * The `FlutterEngine` instance for this view controller. This could be the engine this + * `FlutterViewController` is initialized with or a new `FlutterEngine` implicitly created if + * no engine was supplied during initialization. */ @property(weak, nonatomic, readonly) FlutterEngine* engine; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 42a4081cca13c..3a6d113b5e48b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -201,7 +201,7 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { #pragma mark - Selectors handling -- (void)addApplicationLifeCycleDelegate:(NSObject*)delegate { +- (void)addApplicationLifeCycleDelegate:(NSObject*)delegate { [_lifeCycleDelegate addDelegate:delegate]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index c267518929685..c3302194b039a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -122,6 +122,11 @@ - (instancetype)initWithName:(NSString*)labelPrefix name:UIApplicationWillResignActiveNotification object:nil]; + [center addObserver:self + selector:@selector(onLocaleUpdated:) + name:NSCurrentLocaleDidChangeNotification + object:nil]; + return self; } @@ -195,9 +200,9 @@ - (void)dispatchPointerDataPacket:(std::unique_ptr)p return _shell->GetTaskRunners().GetPlatformTaskRunner(); } -- (fml::RefPtr)GPUTaskRunner { +- (fml::RefPtr)RasterTaskRunner { FML_DCHECK(_shell); - return _shell->GetTaskRunners().GetGPUTaskRunner(); + return _shell->GetTaskRunners().GetRasterTaskRunner(); } - (void)ensureSemanticsEnabled { @@ -462,7 +467,7 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { flutter::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - fml::MessageLoop::GetCurrent().GetTaskRunner(), // gpu + fml::MessageLoop::GetCurrent().GetTaskRunner(), // raster _threadHost.ui_thread->GetTaskRunner(), // ui _threadHost.io_thread->GetTaskRunner() // io ); @@ -476,7 +481,7 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { } else { flutter::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform - _threadHost.gpu_thread->GetTaskRunner(), // gpu + _threadHost.raster_thread->GetTaskRunner(), // raster _threadHost.ui_thread->GetTaskRunner(), // ui _threadHost.io_thread->GetTaskRunner() // io ); @@ -494,6 +499,7 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { << entrypoint.UTF8String; } else { [self setupChannels]; + [self onLocaleUpdated:nil]; if (!_platformViewsController) { _platformViewsController.reset(new flutter::FlutterPlatformViewsController()); } @@ -592,6 +598,13 @@ - (void)performAction:(FlutterTextInputAction)action withClient:(int)client { arguments:@[ @(client), actionString ]]; } +- (void)showAutocorrectionPromptRectForStart:(NSUInteger)start + end:(NSUInteger)end + withClient:(int)client { + [_textInputChannel.get() invokeMethod:@"TextInputClient.showAutocorrectionPromptRect" + arguments:@[ @(client), @(start), @(end) ]]; +} + #pragma mark - Screenshot Delegate - (flutter::Rasterizer::Screenshot)takeScreenshot:(flutter::Rasterizer::ScreenshotType)type @@ -710,6 +723,48 @@ - (void)setIsGpuDisabled:(BOOL)value { _isGpuDisabled = value; } +#pragma mark - Locale updates + +- (void)onLocaleUpdated:(NSNotification*)notification { + NSArray* preferredLocales = [NSLocale preferredLanguages]; + NSMutableArray* data = [[NSMutableArray new] autorelease]; + + // Force prepend the [NSLocale currentLocale] to the front of the list + // to ensure we are including the full default locale. preferredLocales + // is not guaranteed to include anything beyond the languageCode. + NSLocale* currentLocale = [NSLocale currentLocale]; + NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; + NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; + NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; + NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; + if (languageCode) { + [data addObject:languageCode]; + [data addObject:(countryCode ? countryCode : @"")]; + [data addObject:(scriptCode ? scriptCode : @"")]; + [data addObject:(variantCode ? variantCode : @"")]; + } + + // Add any secondary locales/languages to the list. + for (NSString* localeID in preferredLocales) { + NSLocale* currentLocale = [[[NSLocale alloc] initWithLocaleIdentifier:localeID] autorelease]; + NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; + NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; + NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; + NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; + if (!languageCode) { + continue; + } + [data addObject:languageCode]; + [data addObject:(countryCode ? countryCode : @"")]; + [data addObject:(scriptCode ? scriptCode : @"")]; + [data addObject:(variantCode ? variantCode : @"")]; + } + if (data.count == 0) { + return; + } + [self.localizationChannel invokeMethod:@"setLocale" arguments:data]; +} + @end @implementation FlutterEngineRegistrar { @@ -776,8 +831,8 @@ - (void)registerViewFactory:(NSObject*)factory withId:(NSString*)factoryId gestureRecognizersBlockingPolicy: (FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy { - [_flutterEngine platformViewsController] -> RegisterViewFactory(factory, factoryId, - gestureRecognizersBlockingPolicy); + [_flutterEngine platformViewsController]->RegisterViewFactory(factory, factoryId, + gestureRecognizersBlockingPolicy); } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 277950f9d68d0..e995b6f77f9c9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -32,7 +32,7 @@ - (void)dispatchPointerDataPacket:(std::unique_ptr)packet; - (fml::RefPtr)platformTaskRunner; -- (fml::RefPtr)GPUTaskRunner; +- (fml::RefPtr)RasterTaskRunner; - (fml::WeakPtr)platformView; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm index 11c0d60618886..4127061f3e7de 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm @@ -39,6 +39,7 @@ - (instancetype)init { if (self) { self.layer.opaque = NO; self.userInteractionEnabled = NO; + self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); } return self; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 809c467a4e554..5238be44eaef1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -4,6 +4,7 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #include "flutter/fml/logging.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #include #include @@ -161,6 +162,15 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { // UIViewControllerBasedStatusBarAppearance [UIApplication sharedApplication].statusBarHidden = ![overlays containsObject:@"SystemUiOverlay.top"]; + if ([overlays containsObject:@"SystemUiOverlay.bottom"]) { + [[NSNotificationCenter defaultCenter] + postNotificationName:FlutterViewControllerShowHomeIndicator + object:nil]; + } else { + [[NSNotificationCenter defaultCenter] + postNotificationName:FlutterViewControllerHideHomeIndicator + object:nil]; + } } - (void)restoreSystemChromeSystemUIOverlays { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b86c4623fa7a7..14a45142eb9b3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -8,16 +8,96 @@ #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/ios_surface_gl.h" +#include #include #include #include #include "FlutterPlatformViews_Internal.h" +#include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/shell/common/persistent_cache.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" namespace flutter { +std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( + GrContext* gr_context, + std::shared_ptr ios_context) { + if (available_layer_index_ >= layers_.size()) { + std::shared_ptr layer; + fml::scoped_nsobject overlay_view; + fml::scoped_nsobject overlay_view_wrapper; + + if (!gr_context) { + overlay_view.reset([[FlutterOverlayView alloc] init]); + overlay_view_wrapper.reset([[FlutterOverlayView alloc] init]); + + std::unique_ptr ios_surface = + [overlay_view.get() createSurface:std::move(ios_context)]; + std::unique_ptr surface = ios_surface->CreateGPUSurface(); + + layer = std::make_shared( + std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface), + std::move(surface)); + } else { + CGFloat screenScale = [UIScreen mainScreen].scale; + overlay_view.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]); + overlay_view_wrapper.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]); + + std::unique_ptr ios_surface = + [overlay_view.get() createSurface:std::move(ios_context)]; + std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); + + layer = std::make_shared( + std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface), + std::move(surface)); + layer->gr_context = gr_context; + } + // The overlay view wrapper masks the overlay view. + // This is required to keep the backing surface size unchanged between frames. + // + // Otherwise, changing the size of the overlay would require a new surface, + // which can be very expensive. + // + // This is the case of an animation in which the overlay size is changing in every frame. + // + // +------------------------+ + // | overlay_view | + // | +--------------+ | +--------------+ + // | | wrapper | | == mask => | overlay_view | + // | +--------------+ | +--------------+ + // +------------------------+ + overlay_view_wrapper.get().clipsToBounds = YES; + [overlay_view_wrapper.get() addSubview:overlay_view]; + layers_.push_back(layer); + } + std::shared_ptr layer = layers_[available_layer_index_]; + if (gr_context != layer->gr_context) { + layer->gr_context = gr_context; + // The overlay already exists, but the GrContext was changed so we need to recreate + // the rendering surface with the new GrContext. + IOSSurface* ios_surface = layer->ios_surface.get(); + std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); + layer->surface = std::move(surface); + } + available_layer_index_++; + return layer; +} + +void FlutterPlatformViewLayerPool::RecycleLayers() { + available_layer_index_ = 0; +} + +std::vector> +FlutterPlatformViewLayerPool::GetUnusedLayers() { + std::vector> results; + for (size_t i = available_layer_index_; i < layers_.size(); i++) { + results.push_back(layers_[i]); + } + return results; +} + void FlutterPlatformViewsController::SetFlutterView(UIView* flutter_view) { flutter_view_.reset([flutter_view retain]); } @@ -83,6 +163,9 @@ NSObject* embedded_view = [factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:params]; + // Set a unique view identifier, so the platform view can be identified in unit tests. + [embedded_view view].accessibilityIdentifier = + [NSString stringWithFormat:@"platform_view[%ld]", viewId]; views_[viewId] = fml::scoped_nsobject>([embedded_view retain]); FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc] @@ -165,10 +248,13 @@ } void FlutterPlatformViewsController::CancelFrame() { - composition_order_.clear(); + composition_order_ = active_composition_order_; } bool FlutterPlatformViewsController::HasPendingViewOperations() { + if (!views_to_dispose_.empty()) { + return true; + } if (!views_to_recomposite_.empty()) { return true; } @@ -178,14 +264,15 @@ const int FlutterPlatformViewsController::kDefaultMergedLeaseDuration; PostPrerollResult FlutterPlatformViewsController::PostPrerollAction( - fml::RefPtr gpu_thread_merger) { + fml::RefPtr raster_thread_merger) { const bool uiviews_mutated = HasPendingViewOperations(); if (uiviews_mutated) { - if (gpu_thread_merger->IsMerged()) { - gpu_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration); + if (raster_thread_merger->IsMerged()) { + raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration); } else { + // Wait until |EndFrame| to merge the threads. + merge_threads_ = true; CancelFrame(); - gpu_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); return PostPrerollResult::kResubmitFrame; } } @@ -196,8 +283,11 @@ int view_id, std::unique_ptr params) { picture_recorders_[view_id] = std::make_unique(); - picture_recorders_[view_id]->beginRecording(SkRect::Make(frame_size_)); - picture_recorders_[view_id]->getRecordingCanvas()->clear(SK_ColorTRANSPARENT); + + auto rtree_factory = RTreeFactory(); + platform_view_rtrees_[view_id] = rtree_factory.getInstance(); + picture_recorders_[view_id]->beginRecording(SkRect::Make(frame_size_), &rtree_factory); + composition_order_.push_back(view_id); if (current_composition_params_.count(view_id) == 1 && @@ -357,81 +447,219 @@ [sub_view removeFromSuperview]; } views_.clear(); - overlays_.clear(); composition_order_.clear(); active_composition_order_.clear(); picture_recorders_.clear(); + platform_view_rtrees_.clear(); current_composition_params_.clear(); clip_count_.clear(); views_to_recomposite_.clear(); + layer_pool_->RecycleLayers(); +} + +SkRect FlutterPlatformViewsController::GetPlatformViewRect(int view_id) { + UIView* platform_view = [views_[view_id].get() view]; + UIScreen* screen = [UIScreen mainScreen]; + CGRect platform_view_cgrect = [platform_view convertRect:platform_view.bounds + toView:flutter_view_]; + return SkRect::MakeXYWH(platform_view_cgrect.origin.x * screen.scale, // + platform_view_cgrect.origin.y * screen.scale, // + platform_view_cgrect.size.width * screen.scale, // + platform_view_cgrect.size.height * screen.scale // + ); } bool FlutterPlatformViewsController::SubmitFrame(GrContext* gr_context, - std::shared_ptr ios_context) { + std::shared_ptr ios_context, + SkCanvas* background_canvas) { + if (merge_threads_) { + // Threads are about to be merged, we drop everything from this frame + // and possibly resubmit the same layer tree in the next frame. + // Before merging thread, we know the code is not running on the main thread. Assert that + FML_DCHECK(![[NSThread currentThread] isMainThread]); + picture_recorders_.clear(); + composition_order_.clear(); + return true; + } + + // Any UIKit related code has to run on main thread. + // When on a non-main thread, we only allow the rest of the method to run if there is no + // Pending UIView operations. + FML_DCHECK([[NSThread currentThread] isMainThread] || !HasPendingViewOperations()); + DisposeViews(); - bool did_submit = true; - for (int64_t view_id : composition_order_) { - EnsureOverlayInitialized(view_id, ios_context, gr_context); - auto frame = overlays_[view_id]->surface->AcquireFrame(frame_size_); - // If frame is null, AcquireFrame already printed out an error message. - if (frame) { - SkCanvas* canvas = frame->SkiaCanvas(); - canvas->drawPicture(picture_recorders_[view_id]->finishRecordingAsPicture()); - canvas->flush(); - did_submit &= frame->Submit(); + // Resolve all pending GPU operations before allocating a new surface. + background_canvas->flush(); + // Clipping the background canvas before drawing the picture recorders requires to + // save and restore the clip context. + SkAutoCanvasRestore save(background_canvas, /*doSave=*/true); + // Maps a platform view id to a vector of `FlutterPlatformViewLayer`. + LayersMap platform_view_layers; + + auto did_submit = true; + auto num_platform_views = composition_order_.size(); + + for (size_t i = 0; i < num_platform_views; i++) { + int64_t platform_view_id = composition_order_[i]; + sk_sp rtree = platform_view_rtrees_[platform_view_id]; + sk_sp picture = picture_recorders_[platform_view_id]->finishRecordingAsPicture(); + + // Check if the current picture contains overlays that intersect with the + // current platform view or any of the previous platform views. + for (size_t j = i + 1; j > 0; j--) { + int64_t current_platform_view_id = composition_order_[j - 1]; + SkRect platform_view_rect = GetPlatformViewRect(current_platform_view_id); + std::list intersection_rects = + rtree->searchNonOverlappingDrawnRects(platform_view_rect); + auto allocation_size = intersection_rects.size(); + + // For testing purposes, the overlay id is used to find the overlay view. + // This is the index of the layer for the current platform view. + auto overlay_id = platform_view_layers[current_platform_view_id].size(); + + // If the max number of allocations per platform view is exceeded, + // then join all the rects into a single one. + // + // TODO(egarciad): Consider making this configurable. + // https://github.com/flutter/flutter/issues/52510 + if (allocation_size > kMaxLayerAllocations) { + SkRect joined_rect; + for (const SkRect& rect : intersection_rects) { + joined_rect.join(rect); + } + // Replace the rects in the intersection rects list for a single rect that is + // the union of all the rects in the list. + intersection_rects.clear(); + intersection_rects.push_back(joined_rect); + } + for (SkRect& joined_rect : intersection_rects) { + // Get the intersection rect between the current rect + // and the platform view rect. + joined_rect.intersect(platform_view_rect); + // Subpixels in the platform may not align with the canvas subpixels. + // To workaround it, round the floating point bounds and make the rect slighly larger. + // For example, {0.3, 0.5, 3.1, 4.7} becomes {0, 0, 4, 5}. + joined_rect.setLTRB(std::floor(joined_rect.left()), std::floor(joined_rect.top()), + std::ceil(joined_rect.right()), std::ceil(joined_rect.bottom())); + // Clip the background canvas, so it doesn't contain any of the pixels drawn + // on the overlay layer. + background_canvas->clipRect(joined_rect, SkClipOp::kDifference); + // Get a new host layer. + std::shared_ptr layer = GetLayer(gr_context, // + ios_context, // + picture, // + joined_rect, // + current_platform_view_id, // + overlay_id // + ); + did_submit &= layer->did_submit_last_frame; + platform_view_layers[current_platform_view_id].push_back(layer); + overlay_id++; + } } - } - picture_recorders_.clear(); - if (composition_order_ == active_composition_order_) { - composition_order_.clear(); - return did_submit; - } - DetachUnusedLayers(); - active_composition_order_.clear(); - UIView* flutter_view = flutter_view_.get(); + background_canvas->drawPicture(picture); + } + // If a layer was allocated in the previous frame, but it's not used in the current frame, + // then it can be removed from the scene. + RemoveUnusedLayers(); + // Organize the layers by their z indexes. + BringLayersIntoView(platform_view_layers); + // Mark all layers as available, so they can be used in the next frame. + layer_pool_->RecycleLayers(); + // Reset the composition order, so next frame starts empty. + composition_order_.clear(); + return did_submit; +} + +void FlutterPlatformViewsController::BringLayersIntoView(LayersMap layer_map) { + UIView* flutter_view = flutter_view_.get(); + auto zIndex = 0; for (size_t i = 0; i < composition_order_.size(); i++) { - int view_id = composition_order_[i]; - // We added a chain of super views to the platform view to handle clipping. - // The `platform_view_root` is the view at the top of the chain which is a direct subview of the - // `FlutterView`. - UIView* platform_view_root = root_views_[view_id].get(); - UIView* overlay = overlays_[view_id]->overlay_view; - FML_CHECK(platform_view_root.superview == overlay.superview); - if (platform_view_root.superview == flutter_view) { - [flutter_view bringSubviewToFront:platform_view_root]; - [flutter_view bringSubviewToFront:overlay]; - } else { + int64_t platform_view_id = composition_order_[i]; + std::vector> layers = layer_map[platform_view_id]; + UIView* platform_view_root = root_views_[platform_view_id].get(); + + if (platform_view_root.superview != flutter_view) { [flutter_view addSubview:platform_view_root]; - [flutter_view addSubview:overlay]; - overlay.frame = flutter_view.bounds; + } else { + platform_view_root.layer.zPosition = zIndex++; + } + for (const std::shared_ptr& layer : layers) { + if ([layer->overlay_view_wrapper superview] != flutter_view) { + [flutter_view addSubview:layer->overlay_view_wrapper]; + } else { + layer->overlay_view_wrapper.get().layer.zPosition = zIndex++; + } } + active_composition_order_.push_back(platform_view_id); + } +} - active_composition_order_.push_back(view_id); +void FlutterPlatformViewsController::EndFrame( + fml::RefPtr raster_thread_merger) { + if (merge_threads_) { + raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); + merge_threads_ = false; } - composition_order_.clear(); - return did_submit; } -void FlutterPlatformViewsController::DetachUnusedLayers() { - std::unordered_set composition_order_set; +std::shared_ptr FlutterPlatformViewsController::GetLayer( + GrContext* gr_context, + std::shared_ptr ios_context, + sk_sp picture, + SkRect rect, + int64_t view_id, + int64_t overlay_id) { + std::shared_ptr layer = layer_pool_->GetLayer(gr_context, ios_context); + + UIView* overlay_view_wrapper = layer->overlay_view_wrapper.get(); + auto screenScale = [UIScreen mainScreen].scale; + // Set the size of the overlay view wrapper. + // This wrapper view masks the overlay view. + overlay_view_wrapper.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale, + rect.width() / screenScale, rect.height() / screenScale); + // Set a unique view identifier, so the overlay wrapper can be identified in unit tests. + overlay_view_wrapper.accessibilityIdentifier = + [NSString stringWithFormat:@"platform_view[%lld].overlay[%lld]", view_id, overlay_id]; + + UIView* overlay_view = layer->overlay_view.get(); + // Set the size of the overlay view. + // This size is equal to the the device screen size. + overlay_view.frame = flutter_view_.get().bounds; + + std::unique_ptr frame = layer->surface->AcquireFrame(frame_size_); + // If frame is null, AcquireFrame already printed out an error message. + if (!frame) { + return layer; + } + SkCanvas* overlay_canvas = frame->SkiaCanvas(); + overlay_canvas->clear(SK_ColorTRANSPARENT); + // Offset the picture since its absolute position on the scene is determined + // by the position of the overlay view. + overlay_canvas->translate(-rect.x(), -rect.y()); + overlay_canvas->drawPicture(picture); + + layer->did_submit_last_frame = frame->Submit(); + return layer; +} + +void FlutterPlatformViewsController::RemoveUnusedLayers() { + std::vector> layers = layer_pool_->GetUnusedLayers(); + for (const std::shared_ptr& layer : layers) { + [layer->overlay_view_wrapper removeFromSuperview]; + } + std::unordered_set composition_order_set; for (int64_t view_id : composition_order_) { composition_order_set.insert(view_id); } - + // Remove unused platform views. for (int64_t view_id : active_composition_order_) { if (composition_order_set.find(view_id) == composition_order_set.end()) { - if (root_views_.find(view_id) == root_views_.end()) { - continue; - } - // We added a chain of super views to the platform view to handle clipping. - // The `platform_view_root` is the view at the top of the chain which is a direct subview of - // the `FlutterView`. UIView* platform_view_root = root_views_[view_id].get(); [platform_view_root removeFromSuperview]; - [overlays_[view_id]->overlay_view.get() removeFromSuperview]; } } } @@ -441,16 +669,14 @@ return; } + FML_DCHECK([[NSThread currentThread] isMainThread]); + for (int64_t viewId : views_to_dispose_) { UIView* root_view = root_views_[viewId].get(); [root_view removeFromSuperview]; views_.erase(viewId); touch_interceptors_.erase(viewId); root_views_.erase(viewId); - if (overlays_.find(viewId) != overlays_.end()) { - [overlays_[viewId]->overlay_view.get() removeFromSuperview]; - } - overlays_.erase(viewId); current_composition_params_.erase(viewId); clip_count_.erase(viewId); views_to_recomposite_.erase(viewId); @@ -458,56 +684,6 @@ views_to_dispose_.clear(); } -void FlutterPlatformViewsController::EnsureOverlayInitialized( - int64_t overlay_id, - std::shared_ptr ios_context, - GrContext* gr_context) { - FML_DCHECK(flutter_view_); - - auto overlay_it = overlays_.find(overlay_id); - - if (!gr_context) { - if (overlays_.count(overlay_id) != 0) { - return; - } - fml::scoped_nsobject overlay_view([[FlutterOverlayView alloc] init]); - overlay_view.get().frame = flutter_view_.get().bounds; - overlay_view.get().autoresizingMask = - (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - std::unique_ptr ios_surface = - [overlay_view.get() createSurface:std::move(ios_context)]; - std::unique_ptr surface = ios_surface->CreateGPUSurface(); - overlays_[overlay_id] = std::make_unique( - std::move(overlay_view), std::move(ios_surface), std::move(surface)); - return; - } - - if (overlay_it != overlays_.end()) { - FlutterPlatformViewLayer* overlay = overlay_it->second.get(); - if (gr_context != overlay->gr_context) { - overlay->gr_context = gr_context; - // The overlay already exists, but the GrContext was changed so we need to recreate - // the rendering surface with the new GrContext. - IOSSurface* ios_surface = overlay_it->second->ios_surface.get(); - std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); - overlay_it->second->surface = std::move(surface); - } - return; - } - auto contentsScale = flutter_view_.get().layer.contentsScale; - fml::scoped_nsobject overlay_view( - [[FlutterOverlayView alloc] initWithContentsScale:contentsScale]); - overlay_view.get().frame = flutter_view_.get().bounds; - overlay_view.get().autoresizingMask = - (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - std::unique_ptr ios_surface = - [overlay_view.get() createSurface:std::move(ios_context)]; - std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); - overlays_[overlay_id] = std::make_unique( - std::move(overlay_view), std::move(ios_surface), std::move(surface)); - overlays_[overlay_id]->gr_context = gr_context; -} - } // namespace flutter // This recognizers delays touch events from being dispatched to the responder chain until it failed diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index d135d7d2ac290..0dcb793b00f03 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ #include "flutter/flow/embedded_views.h" +#include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/shell/common/shell.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" @@ -59,21 +60,63 @@ class IOSSurface; struct FlutterPlatformViewLayer { FlutterPlatformViewLayer(fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, std::unique_ptr ios_surface, std::unique_ptr surface); ~FlutterPlatformViewLayer(); fml::scoped_nsobject overlay_view; + fml::scoped_nsobject overlay_view_wrapper; std::unique_ptr ios_surface; std::unique_ptr surface; + // Whether a frame for this layer was submitted. + bool did_submit_last_frame; + // The GrContext that is currently used by the overlay surfaces. // We track this to know when the GrContext for the Flutter app has changed // so we can update the overlay with the new context. GrContext* gr_context; }; +// This class isn't thread safe. +class FlutterPlatformViewLayerPool { + public: + FlutterPlatformViewLayerPool() = default; + ~FlutterPlatformViewLayerPool() = default; + + // Gets a layer from the pool if available, or allocates a new one. + // Finally, it marks the layer as used. That is, it increments `available_layer_index_`. + std::shared_ptr GetLayer(GrContext* gr_context, + std::shared_ptr ios_context); + + // Gets the layers in the pool that aren't currently used. + // This method doesn't mark the layers as unused. + std::vector> GetUnusedLayers(); + + // Marks the layers in the pool as available for reuse. + void RecycleLayers(); + + private: + // The index of the entry in the layers_ vector that determines the beginning of the unused + // layers. For example, consider the following vector: + // _____ + // | 0 | + /// |---| + /// | 1 | <-- available_layer_index_ + /// |---| + /// | 2 | + /// |---| + /// + /// This indicates that entries starting from 1 can be reused meanwhile the entry at position 0 + /// cannot be reused. + size_t available_layer_index_ = 0; + std::vector> layers_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewLayerPool); +}; + class FlutterPlatformViewsController { public: FlutterPlatformViewsController(); @@ -91,6 +134,8 @@ class FlutterPlatformViewsController { void SetFrameSize(SkISize frame_size); + // Indicates that we don't compisite any platform views or overlays during this frame. + // Also reverts the composition_order_ to its original state at the begining of the frame. void CancelFrame(); void PrerollCompositeEmbeddedView(int view_id, @@ -103,20 +148,48 @@ class FlutterPlatformViewsController { // returns nil. NSObject* GetPlatformViewByID(int view_id); - PostPrerollResult PostPrerollAction(fml::RefPtr gpu_thread_merger); + PostPrerollResult PostPrerollAction(fml::RefPtr raster_thread_merger); std::vector GetCurrentCanvases(); SkCanvas* CompositeEmbeddedView(int view_id); + // The rect of the platform view at index view_id. This rect has been translated into the + // host view coordinate system. Units are device screen pixels. + SkRect GetPlatformViewRect(int view_id); + // Discards all platform views instances and auxiliary resources. void Reset(); - bool SubmitFrame(GrContext* gr_context, std::shared_ptr ios_context); + bool SubmitFrame(GrContext* gr_context, + std::shared_ptr ios_context, + SkCanvas* background_canvas); + + // Invoked at the very end of a frame. + // After invoking this method, nothing should happen on the current TaskRunner during the same + // frame. + void EndFrame(fml::RefPtr raster_thread_merger); void OnMethodCall(FlutterMethodCall* call, FlutterResult& result); private: + static const size_t kMaxLayerAllocations = 2; + + using LayersMap = std::map>>; + + // The pool of reusable view layers. The pool allows to recycle layer in each frame. + std::unique_ptr layer_pool_; + + // The platform view's R-tree keyed off the view id, which contains any subsequent + // draw operation until the next platform view or the last leaf node in the layer tree. + // + // The R-trees are deleted by the FlutterPlatformViewsController.reset(). + std::map> platform_view_rtrees_; + + // The platform view's picture recorder keyed off the view id, which contains any subsequent + // operation until the next platform view or the end of the last leaf node in the layer tree. + std::map> picture_recorders_; + fml::scoped_nsobject channel_; fml::scoped_nsobject flutter_view_; fml::scoped_nsobject flutter_view_controller_; @@ -134,7 +207,6 @@ class FlutterPlatformViewsController { // Mapping a platform view ID to the count of the clipping operations that were applied to the // platform view last time it was composited. std::map clip_count_; - std::map> overlays_; SkISize frame_size_; // This is the number of frames the task runners will stay @@ -163,19 +235,12 @@ class FlutterPlatformViewsController { std::map gesture_recognizers_blocking_policies; - std::map> picture_recorders_; - void OnCreate(FlutterMethodCall* call, FlutterResult& result); void OnDispose(FlutterMethodCall* call, FlutterResult& result); void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result); void OnRejectGesture(FlutterMethodCall* call, FlutterResult& result); - - void DetachUnusedLayers(); // Dispose the views in `views_to_dispose_`. void DisposeViews(); - void EnsureOverlayInitialized(int64_t overlay_id, - std::shared_ptr ios_context, - GrContext* gr_context); // This will return true after pre-roll if any of the embedded views // have mutated for last layer tree. @@ -215,6 +280,25 @@ class FlutterPlatformViewsController { void ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view); void CompositeWithParams(int view_id, const EmbeddedViewParams& params); + // Default to `false`. + // If `true`, gpu thread and platform thread should be merged during |EndFrame|. + // Always resets to `false` right after the threads are merged. + bool merge_threads_ = false; + // Allocates a new FlutterPlatformViewLayer if needed, draws the pixels within the rect from + // the picture on the layer's canvas. + std::shared_ptr GetLayer(GrContext* gr_context, + std::shared_ptr ios_context, + sk_sp picture, + SkRect rect, + int64_t view_id, + int64_t overlay_id); + // Removes overlay views and platform views that aren't needed in the current frame. + // Must run on the platform thread. + void RemoveUnusedLayers(); + // Appends the overlay views and platform view and sets their z index based on the composition + // order. + void BringLayersIntoView(LayersMap layer_map); + FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewsController); }; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 9310fa1803f11..551535a2c7faf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -11,16 +11,20 @@ namespace flutter { -FlutterPlatformViewLayer::FlutterPlatformViewLayer(fml::scoped_nsobject overlay_view, - std::unique_ptr ios_surface, - std::unique_ptr surface) +FlutterPlatformViewLayer::FlutterPlatformViewLayer( + fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, + std::unique_ptr ios_surface, + std::unique_ptr surface) : overlay_view(std::move(overlay_view)), + overlay_view_wrapper(std::move(overlay_view_wrapper)), ios_surface(std::move(ios_surface)), surface(std::move(surface)){}; FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default; -FlutterPlatformViewsController::FlutterPlatformViewsController() = default; +FlutterPlatformViewsController::FlutterPlatformViewsController() + : layer_pool_(std::make_unique()){}; FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h index d3451b5bd4cd4..8446a79946d63 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h @@ -34,7 +34,9 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) { - (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state withClient:(int)client withPosition:(NSDictionary*)point; - +- (void)showAutocorrectionPromptRectForStart:(NSUInteger)start + end:(NSUInteger)end + withClient:(int)client; @end #endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h index 70bd949579ce2..3677c0f78284a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -36,6 +36,9 @@ @end /** A range of text in the buffer of a Flutter text editing widget. */ +#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG +FLUTTER_EXPORT +#endif @interface FlutterTextRange : UITextRange @property(nonatomic, readonly) NSRange range; @@ -44,4 +47,32 @@ @end +#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG +FLUTTER_EXPORT +#endif +@interface FlutterTextInputView : UIView + +// UITextInput +@property(nonatomic, readonly) NSMutableString* text; +@property(nonatomic, readonly) NSMutableString* markedText; +@property(readwrite, copy) UITextRange* selectedTextRange; +@property(nonatomic, strong) UITextRange* markedTextRange; +@property(nonatomic, copy) NSDictionary* markedTextStyle; +@property(nonatomic, assign) id inputDelegate; + +// UITextInputTraits +@property(nonatomic) UITextAutocapitalizationType autocapitalizationType; +@property(nonatomic) UITextAutocorrectionType autocorrectionType; +@property(nonatomic) UITextSpellCheckingType spellCheckingType; +@property(nonatomic) BOOL enablesReturnKeyAutomatically; +@property(nonatomic) UIKeyboardAppearance keyboardAppearance; +@property(nonatomic) UIKeyboardType keyboardType; +@property(nonatomic) UIReturnKeyType returnKeyType; +@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; +@property(nonatomic) UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0)); +@property(nonatomic) UITextSmartDashesType smartDashesType API_AVAILABLE(ios(11.0)); + +@property(nonatomic, assign) id textInputDelegate; + +@end #endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 029932489f4fd..f9ddd6621d6d8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -140,32 +140,6 @@ - (id)copyWithZone:(NSZone*)zone { @end -@interface FlutterTextInputView : UIView - -// UITextInput -@property(nonatomic, readonly) NSMutableString* text; -@property(nonatomic, readonly) NSMutableString* markedText; -@property(readwrite, copy) UITextRange* selectedTextRange; -@property(nonatomic, strong) UITextRange* markedTextRange; -@property(nonatomic, copy) NSDictionary* markedTextStyle; -@property(nonatomic, assign) id inputDelegate; - -// UITextInputTraits -@property(nonatomic) UITextAutocapitalizationType autocapitalizationType; -@property(nonatomic) UITextAutocorrectionType autocorrectionType; -@property(nonatomic) UITextSpellCheckingType spellCheckingType; -@property(nonatomic) BOOL enablesReturnKeyAutomatically; -@property(nonatomic) UIKeyboardAppearance keyboardAppearance; -@property(nonatomic) UIKeyboardType keyboardType; -@property(nonatomic) UIReturnKeyType returnKeyType; -@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; -@property(nonatomic) UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0)); -@property(nonatomic) UITextSmartDashesType smartDashesType API_AVAILABLE(ios(11.0)); - -@property(nonatomic, assign) id textInputDelegate; - -@end - @implementation FlutterTextInputView { int _textInputClient; const char* _selectionAffinity; @@ -554,6 +528,16 @@ - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection // physical keyboard. - (CGRect)firstRectForRange:(UITextRange*)range { + // multi-stage text is handled somewhere else. + if (_markedTextRange != nil) { + return CGRectZero; + } + + NSUInteger start = ((FlutterTextPosition*)range.start).index; + NSUInteger end = ((FlutterTextPosition*)range.end).index; + [_textInputDelegate showAutocorrectionPromptRectForStart:start + end:end + withClient:_textInputClient]; // TODO(cbracken) Implement. return CGRectZero; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m new file mode 100644 index 0000000000000..6610d9fe4d8cd --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -0,0 +1,32 @@ +// 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. + +#import +#import +#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterTextInputPluginTest : XCTestCase +@end + +@implementation FlutterTextInputPluginTest + +- (void)testAutocorrectionPromptRectAppears { + // Setup test. + id engine = OCMClassMock([FlutterEngine class]); + + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + inputView.textInputDelegate = engine; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + + // Verify behavior. + OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); + + // Clean up mocks + [engine stopMocking]; +} +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/shell/platform/darwin/ios/framework/Source/FlutterView.mm index ed2d1f02b7127..783eafa336f9b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -17,9 +17,9 @@ #include "flutter/shell/platform/darwin/ios/ios_surface_software.h" #include "third_party/skia/include/utils/mac/SkCGUtils.h" -@implementation FlutterView - -id _delegate; +@implementation FlutterView { + id _delegate; +} - (instancetype)init { @throw([NSException exceptionWithName:@"FlutterView must initWithDelegate" diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 6deed5f8104d7..9c0ff5883fa95 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -27,8 +27,11 @@ static constexpr CGFloat kScrollViewContentSize = 2.0; NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate"; - NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc"; +NSNotificationName const FlutterViewControllerHideHomeIndicator = + @"FlutterViewControllerHideHomeIndicator"; +NSNotificationName const FlutterViewControllerShowHomeIndicator = + @"FlutterViewControllerShowHomeIndicator"; /// Class to coalesce calls for a period of time. /// @@ -91,6 +94,7 @@ - (void)invalidate { // just a warning. @interface FlutterViewController () @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI; +@property(nonatomic, assign) BOOL isHomeIndicatorHidden; @end // The following conditional compilation defines an API 13 concept on earlier API targets so that @@ -144,6 +148,14 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { _viewOpaque = YES; + if (engine.viewController) { + FML_LOG(ERROR) << "The supplied FlutterEngine " << [[engine description] UTF8String] + << " is already used with FlutterViewController instance " + << [[engine.viewController description] UTF8String] + << ". One instance of the FlutterEngine can only be attached to one " + "FlutterViewController at a time. Set FlutterEngine.viewController " + "to nil before attaching it to another FlutterViewController."; + } _engine.reset([engine retain]); _engineNeedsLaunch = NO; _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); @@ -271,11 +283,6 @@ - (void)setupNotificationCenterObservers { name:UIKeyboardWillHideNotification object:nil]; - [center addObserver:self - selector:@selector(onLocaleUpdated:) - name:NSCurrentLocaleDidChangeNotification - object:nil]; - [center addObserver:self selector:@selector(onAccessibilityStatusChanged:) name:UIAccessibilityVoiceOverStatusChanged @@ -315,6 +322,16 @@ - (void)setupNotificationCenterObservers { selector:@selector(onUserSettingsChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil]; + + [center addObserver:self + selector:@selector(onHideHomeIndicatorNotification:) + name:FlutterViewControllerHideHomeIndicator + object:nil]; + + [center addObserver:self + selector:@selector(onShowHomeIndicatorNotification:) + name:FlutterViewControllerShowHomeIndicator + object:nil]; } - (void)setInitialRoute:(NSString*)route { @@ -443,9 +460,9 @@ - (void)installFirstFrameCallback { // Start on the platform thread. weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr], platformTaskRunner = [_engine.get() platformTaskRunner], - gpuTaskRunner = [_engine.get() GPUTaskRunner]]() { - FML_DCHECK(gpuTaskRunner->RunsTasksOnCurrentThread()); - // Get callback on GPU thread and jump back to platform thread. + RasterTaskRunner = [_engine.get() RasterTaskRunner]]() { + FML_DCHECK(RasterTaskRunner->RunsTasksOnCurrentThread()); + // Get callback on raster thread and jump back to platform thread. platformTaskRunner->PostTask([weakSelf]() { fml::scoped_nsobject flutterViewController( [(FlutterViewController*)weakSelf.get() retain]); @@ -536,17 +553,18 @@ - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback { #pragma mark - Surface creation and teardown updates - (void)surfaceUpdated:(BOOL)appeared { - // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and GPU thread. + // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster + // thread. if (appeared) { [self installFirstFrameCallback]; - [_engine.get() platformViewsController] -> SetFlutterView(_flutterView.get()); - [_engine.get() platformViewsController] -> SetFlutterViewController(self); - [_engine.get() platformView] -> NotifyCreated(); + [_engine.get() platformViewsController]->SetFlutterView(_flutterView.get()); + [_engine.get() platformViewsController]->SetFlutterViewController(self); + [_engine.get() platformView]->NotifyCreated(); } else { self.displayingFlutterUI = NO; - [_engine.get() platformView] -> NotifyDestroyed(); - [_engine.get() platformViewsController] -> SetFlutterView(nullptr); - [_engine.get() platformViewsController] -> SetFlutterViewController(nullptr); + [_engine.get() platformView]->NotifyDestroyed(); + [_engine.get() platformViewsController]->SetFlutterView(nullptr); + [_engine.get() platformViewsController]->SetFlutterViewController(nullptr); } } @@ -586,7 +604,6 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated { TRACE_EVENT0("flutter", "viewDidAppear"); - [self onLocaleUpdated:nil]; [self onUserSettingsChanged:nil]; [self onAccessibilityStatusChanged:nil]; [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; @@ -705,6 +722,10 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { return flutter::PointerData::Change::kUp; case UITouchPhaseCancelled: return flutter::PointerData::Change::kCancel; + default: + // TODO(53695): Handle the `UITouchPhaseRegion`... enum values. + FML_DLOG(INFO) << "Unhandled touch phase: " << phase; + break; } return flutter::PointerData::Change::kCancel; @@ -718,9 +739,11 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { return flutter::PointerData::DeviceKind::kTouch; case UITouchTypeStylus: return flutter::PointerData::DeviceKind::kStylus; + default: + // TODO(53696): Handle the UITouchTypeIndirectPointer enum value. + FML_DLOG(INFO) << "Unhandled touch type: " << touch.type; + break; } - } else { - return flutter::PointerData::DeviceKind::kTouch; } return flutter::PointerData::DeviceKind::kTouch; @@ -944,13 +967,23 @@ - (void)updateViewportPadding { - (void)keyboardWillChangeFrame:(NSNotification*)notification { NSDictionary* info = [notification userInfo]; - CGFloat bottom = CGRectGetHeight([[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]); - CGFloat scale = [UIScreen mainScreen].scale; + CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect screenRect = [[UIScreen mainScreen] bounds]; + + // Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present + // in the screen to see if the keyboard is visible. + if (CGRectIntersectsRect(keyboardFrame, screenRect)) { + CGFloat bottom = CGRectGetHeight(keyboardFrame); + CGFloat scale = [UIScreen mainScreen].scale; + + // The keyboard is treated as an inset since we want to effectively reduce the window size by + // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming + // bottom padding. + _viewportMetrics.physical_view_inset_bottom = bottom * scale; + } else { + _viewportMetrics.physical_view_inset_bottom = 0; + } - // The keyboard is treated as an inset since we want to effectively reduce the window size by the - // keyboard height. The Dart side will compute a value accounting for the keyboard-consuming - // bottom padding. - _viewportMetrics.physical_view_inset_bottom = bottom * scale; [self updateViewportMetrics]; } @@ -1003,6 +1036,27 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { } } +- (void)onHideHomeIndicatorNotification:(NSNotification*)notification { + self.isHomeIndicatorHidden = YES; +} + +- (void)onShowHomeIndicatorNotification:(NSNotification*)notification { + self.isHomeIndicatorHidden = NO; +} + +- (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator { + if (hideHomeIndicator != _isHomeIndicatorHidden) { + _isHomeIndicatorHidden = hideHomeIndicator; + if (@available(iOS 11, *)) { + [self setNeedsUpdateOfHomeIndicatorAutoHidden]; + } + } +} + +- (BOOL)prefersHomeIndicatorAutoHidden { + return self.isHomeIndicatorHidden; +} + - (BOOL)shouldAutorotate { return YES; } @@ -1039,48 +1093,6 @@ - (void)onAccessibilityStatusChanged:(NSNotification*)notification { #endif } -#pragma mark - Locale updates - -- (void)onLocaleUpdated:(NSNotification*)notification { - NSArray* preferredLocales = [NSLocale preferredLanguages]; - NSMutableArray* data = [[NSMutableArray new] autorelease]; - - // Force prepend the [NSLocale currentLocale] to the front of the list - // to ensure we are including the full default locale. preferredLocales - // is not guaranteed to include anything beyond the languageCode. - NSLocale* currentLocale = [NSLocale currentLocale]; - NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; - NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; - NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; - NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; - if (languageCode) { - [data addObject:languageCode]; - [data addObject:(countryCode ? countryCode : @"")]; - [data addObject:(scriptCode ? scriptCode : @"")]; - [data addObject:(variantCode ? variantCode : @"")]; - } - - // Add any secondary locales/languages to the list. - for (NSString* localeID in preferredLocales) { - NSLocale* currentLocale = [[[NSLocale alloc] initWithLocaleIdentifier:localeID] autorelease]; - NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; - NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; - NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; - NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; - if (!languageCode) { - continue; - } - [data addObject:languageCode]; - [data addObject:(countryCode ? countryCode : @"")]; - [data addObject:(scriptCode ? scriptCode : @"")]; - [data addObject:(variantCode ? variantCode : @"")]; - } - if (data.count == 0) { - return; - } - [[_engine.get() localizationChannel] invokeMethod:@"setLocale" arguments:data]; -} - #pragma mark - Set user settings - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm similarity index 87% rename from shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m rename to shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 09ef7baa023fb..37f3d6068cdc3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -6,6 +6,7 @@ #import #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #include "FlutterBinaryMessenger.h" @@ -27,6 +28,9 @@ @interface MockEngine : NSObject @end @implementation MockEngine +- (FlutterViewController*)viewController { + return nil; +} - (void)setViewController:(FlutterViewController*)viewController { // noop } @@ -118,7 +122,9 @@ - (void)testItReportsPlatformBrightnessWhenViewWillAppear { } - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt { - if (!@available(iOS 13, *)) { + if (@available(iOS 13, *)) { + // noop + } else { return; } @@ -167,7 +173,9 @@ - (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterface #pragma mark - Platform Contrast - (void)testItReportsNormalPlatformContrastByDefault { - if (!@available(iOS 13, *)) { + if (@available(iOS 13, *)) { + // noop + } else { return; } @@ -195,7 +203,9 @@ - (void)testItReportsNormalPlatformContrastByDefault { } - (void)testItReportsPlatformContrastWhenViewWillAppear { - if (!@available(iOS 13, *)) { + if (@available(iOS 13, *)) { + // noop + } else { return; } @@ -223,7 +233,9 @@ - (void)testItReportsPlatformContrastWhenViewWillAppear { } - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt { - if (!@available(iOS 13, *)) { + if (@available(iOS 13, *)) { + // noop + } else { return; } @@ -341,67 +353,67 @@ - (void)testPerformOrientationUpdateDoesNotForceOrientationChange { [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationPortrait didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationPortraitUpsideDown didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationPortrait didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait currentOrientation:UIInterfaceOrientationPortrait didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown currentOrientation:UIInterfaceOrientationPortraitUpsideDown didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO - resultingOrientation:0]; + resultingOrientation:static_cast(0)]; } // Perform an orientation update test that fails when the expected outcome @@ -449,21 +461,18 @@ - (void)testWillDeallocNotification { FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - id observer = - [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification* _Nonnull note) { - [expectation fulfill]; - }]; + [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* _Nonnull note) { + [expectation fulfill]; + }]; realVC = nil; } [self waitForExpectations:@[ expectation ] timeout:1.0]; } - (void)testDoesntLoadViewInInit { - XCTestExpectation* expectation = - [[XCTestExpectation alloc] initWithDescription:@"notification called"]; FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; [engine createShell:@"" libraryURI:@""]; @@ -473,4 +482,17 @@ - (void)testDoesntLoadViewInInit { XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown"); } +- (void)testHideOverlay { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine createShell:@"" libraryURI:@""]; + FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @""); + [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator + object:nil]; + XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @""); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index afe5b8166ce4a..98f222b3254ac 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -5,16 +5,22 @@ #ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_ -#include "flutter/flow/embedded_views.h" #include "flutter/fml/memory/weak_ptr.h" -#include "flutter/fml/platform/darwin/scoped_nsobject.h" -#include "flutter/shell/common/shell.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" -#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" + +namespace flutter { +class FlutterPlatformViewsController; +} FLUTTER_EXPORT extern NSNotificationName const FlutterViewControllerWillDealloc; +FLUTTER_EXPORT +extern NSNotificationName const FlutterViewControllerHideHomeIndicator; + +FLUTTER_EXPORT +extern NSNotificationName const FlutterViewControllerShowHomeIndicator; + @interface FlutterViewController () - (fml::WeakPtr)getWeakPtr; diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h new file mode 100644 index 0000000000000..ba7c4689be463 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h @@ -0,0 +1,158 @@ +// 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 SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_H_ + +#import + +#include "flutter/fml/macros.h" +#include "flutter/fml/memory/weak_ptr.h" +#include "flutter/lib/ui/semantics/semantics_node.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h" + +constexpr int32_t kRootNodeId = 0; + +@class FlutterCustomAccessibilityAction; +@class FlutterPlatformViewSemanticsContainer; + +/** + * A node in the iOS semantics tree. + */ +@interface SemanticsObject : UIAccessibilityElement + +/** + * The globally unique identifier for this node. + */ +@property(nonatomic, readonly) int32_t uid; + +/** + * The parent of this node in the node tree. Will be nil for the root node and + * during transient state changes. + */ +@property(nonatomic, readonly) SemanticsObject* parent; + +/** + * The accessibility bridge that this semantics object is attached to. This + * object may use the bridge to access contextual application information. A weak + * pointer is used because the platform view owns the accessibility bridge. + * If you are referencing this property from an iOS callback, be sure to + * use `isAccessibilityBridgeActive` to protect against the case where this + * node may be orphaned. + */ +@property(nonatomic, readonly) fml::WeakPtr bridge; + +/** + * Due to the fact that VoiceOver may hold onto SemanticObjects even after it shuts down, + * there can be situations where the AccessibilityBridge is shutdown, but the SemanticObject + * will still be alive. If VoiceOver is turned on again, it may try to access this orphaned + * SemanticObject. Methods that are called from the accessiblity framework should use + * this to guard against this case by just returning early if its bridge has been shutdown. + * + * See https://github.com/flutter/flutter/issues/43795 for more information. + */ +- (BOOL)isAccessibilityBridgeAlive; + +/** + * The semantics node used to produce this semantics object. + */ +@property(nonatomic, readonly) flutter::SemanticsNode node; + +/** + * Updates this semantics object using data from the `node` argument. + */ +- (void)setSemanticsNode:(const flutter::SemanticsNode*)node NS_REQUIRES_SUPER; + +/** + * Whether this semantics object has child semantics objects. + */ +@property(nonatomic, readonly) BOOL hasChildren; + +/** + * Direct children of this semantics object. Each child's `parent` property must + * be equal to this object. + */ +@property(nonatomic, strong) NSArray* children; + +/** + * Used if this SemanticsObject is for a platform view. + */ +@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer; + +- (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child; + +- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node; + +#pragma mark - Designated initializers + +- (instancetype)init __attribute__((unavailable("Use initWithBridge instead"))); +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid NS_DESIGNATED_INITIALIZER; + +- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node; +- (void)collectRoutes:(NSMutableArray*)edges; +- (NSString*)routeName; +- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action; + +@end + +/** + * An implementation of UIAccessibilityCustomAction which also contains the + * Flutter uid. + */ +@interface FlutterCustomAccessibilityAction : UIAccessibilityCustomAction + +/** + * The uid of the action defined by the flutter application. + */ +@property(nonatomic) int32_t uid; + +@end + +/** + * The default implementation of `SemanticsObject` for most accessibility elements + * in the iOS accessibility tree. + * + * Use this implementation for nodes that do not need to be expressed via UIKit-specific + * protocols (it only implements NSObject). + * + * See also: + * * TextInputSemanticsObject, which implements `UITextInput` protocol to expose + * editable text widgets to a11y. + */ +@interface FlutterSemanticsObject : SemanticsObject +@end + +/** + * Designated to act as an accessibility container of a platform view. + * + * This object does not take any accessibility actions on its own, nor has any accessibility + * label/value/trait/hint... on its own. The accessibility data will be handled by the platform + * view. + * + * See also: + * * `SemanticsObject` for the other type of semantics objects. + * * `FlutterSemanticsObject` for default implementation of `SemanticsObject`. + */ +@interface FlutterPlatformViewSemanticsContainer : UIAccessibilityElement + +/** + * The position inside an accessibility container. + */ +@property(nonatomic) NSInteger index; + +- (instancetype)init __attribute__((unavailable("Use initWithAccessibilityContainer: instead"))); + +- (instancetype)initWithSemanticsObject:(SemanticsObject*)object; + +@end + +/// A proxy class for SemanticsObject and UISwitch. For most Accessibility and +/// SemanticsObject methods it delegates to the semantics object, otherwise it +/// sends messages to the UISwitch. +@interface FlutterSwitchSemanticsObject : UISwitch +- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject; +@end + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_H_ diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm new file mode 100644 index 0000000000000..19deaf223ee9c --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -0,0 +1,706 @@ +// 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/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" + +#include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" + +namespace { + +flutter::SemanticsAction GetSemanticsActionForScrollDirection( + UIAccessibilityScrollDirection direction) { + // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the + // direction the scroll bar moves in and SemanticsAction uses the direction the finger + // moves in. However, the horizontal scroll direction matches the SemanticsAction direction. + // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal + // maps directly. + switch (direction) { + case UIAccessibilityScrollDirectionRight: + case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using + // _node.textDirection. + return flutter::SemanticsAction::kScrollRight; + case UIAccessibilityScrollDirectionLeft: + case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using + // _node.textDirection. + return flutter::SemanticsAction::kScrollLeft; + case UIAccessibilityScrollDirectionUp: + return flutter::SemanticsAction::kScrollDown; + case UIAccessibilityScrollDirectionDown: + return flutter::SemanticsAction::kScrollUp; + } + FML_DCHECK(false); // Unreachable + return flutter::SemanticsAction::kScrollUp; +} + +} // namespace + +@implementation FlutterSwitchSemanticsObject { + SemanticsObject* _semanticsObject; +} + +- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject { + self = [super init]; + if (self) { + _semanticsObject = [semanticsObject retain]; + } + return self; +} + +- (void)dealloc { + [_semanticsObject release]; + [super dealloc]; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel { + NSMethodSignature* result = [super methodSignatureForSelector:sel]; + if (!result) { + result = [_semanticsObject methodSignatureForSelector:sel]; + } + return result; +} + +- (void)forwardInvocation:(NSInvocation*)anInvocation { + [anInvocation setTarget:_semanticsObject]; + [anInvocation invoke]; +} + +- (CGRect)accessibilityFrame { + return [_semanticsObject accessibilityFrame]; +} + +- (id)accessibilityContainer { + return [_semanticsObject accessibilityContainer]; +} + +- (NSString*)accessibilityLabel { + return [_semanticsObject accessibilityLabel]; +} + +- (NSString*)accessibilityHint { + return [_semanticsObject accessibilityHint]; +} + +- (NSString*)accessibilityValue { + if ([_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsToggled) || + [_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { + self.on = YES; + } else { + self.on = NO; + } + + if (![_semanticsObject isAccessibilityBridgeAlive]) { + return nil; + } else { + return [super accessibilityValue]; + } +} + +@end // FlutterSwitchSemanticsObject + +@implementation FlutterCustomAccessibilityAction { +} +@end + +/** + * Represents a semantics object that has children and hence has to be presented to the OS as a + * UIAccessibilityContainer. + * + * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an + * object that returns YES for isAccessibilityElement cannot also implement + * UIAccessibilityContainer. + * + * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from + * the framework, such as: + * + * SemanticsObject1 + * SemanticsObject2 + * SemanticsObject3 + * SemanticsObject4 + * + * is translated into the following hierarchy, which is understood by iOS: + * + * SemanticsObjectContainer1 + * SemanticsObject1 + * SemanticsObjectContainer2 + * SemanticsObject2 + * SemanticsObject3 + * SemanticsObject4 + * + * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the + * world (second tree) as follows: We replace each SemanticsObjects that has children with a + * SemanticsObjectContainer, which has the original SemanticsObject and its children as children. + * + * SemanticsObjects have semantic information attached to them which is interpreted by + * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just + * there for structure and they don't provide any semantic information to VoiceOver (they return + * NO for isAccessibilityElement). + */ +@interface SemanticsObjectContainer : UIAccessibilityElement +- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead"))); +- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject + bridge:(fml::WeakPtr)bridge + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, weak) SemanticsObject* semanticsObject; + +@end + +@interface SemanticsObject () +/** Should only be called in conjunction with setting child/parent relationship. */ +- (void)privateSetParent:(SemanticsObject*)parent; +@end + +@implementation SemanticsObject { + fml::scoped_nsobject _container; + NSMutableArray* _children; +} + +#pragma mark - Override base class designated initializers + +// Method declared as unavailable in the interface +- (instancetype)init { + [self release]; + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +#pragma mark - Designated initializers + +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid { + FML_DCHECK(bridge) << "bridge must be set"; + FML_DCHECK(uid >= kRootNodeId); + // Initialize with the UIView as the container. + // The UIView will not necessarily be accessibility parent for this object. + // The bridge informs the OS of the actual structure via + // `accessibilityContainer` and `accessibilityElementAtIndex`. + self = [super initWithAccessibilityContainer:bridge->view()]; + + if (self) { + _bridge = bridge; + _uid = uid; + _children = [[NSMutableArray alloc] init]; + } + + return self; +} + +- (void)dealloc { + for (SemanticsObject* child in _children) { + [child privateSetParent:nil]; + } + [_children removeAllObjects]; + [_children release]; + _parent = nil; + _container.get().semanticsObject = nil; + [_platformViewSemanticsContainer release]; + [super dealloc]; +} + +#pragma mark - Semantic object methods + +- (BOOL)isAccessibilityBridgeAlive { + return [self bridge].get() != nil; +} + +- (void)setSemanticsNode:(const flutter::SemanticsNode*)node { + _node = *node; +} + +/** + * Whether calling `setSemanticsNode:` with `node` would cause a layout change. + */ +- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node { + return [self node].rect != node->rect || [self node].transform != node->transform; +} + +/** + * Whether calling `setSemanticsNode:` with `node` would cause a scroll event. + */ +- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node { + return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) && + [self node].scrollPosition != node->scrollPosition; +} + +- (BOOL)hasChildren { + if (_node.IsPlatformViewNode()) { + return YES; + } + return [self.children count] != 0; +} + +- (void)privateSetParent:(SemanticsObject*)parent { + _parent = parent; +} + +- (void)setChildren:(NSArray*)children { + for (SemanticsObject* child in _children) { + [child privateSetParent:nil]; + } + [_children release]; + _children = [[NSMutableArray alloc] initWithArray:children]; + for (SemanticsObject* child in _children) { + [child privateSetParent:self]; + } +} + +- (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child { + SemanticsObject* oldChild = _children[index]; + [oldChild privateSetParent:nil]; + [child privateSetParent:self]; + [_children replaceObjectAtIndex:index withObject:child]; +} + +#pragma mark - UIAccessibility overrides + +- (BOOL)isAccessibilityElement { + if (![self isAccessibilityBridgeAlive]) + return false; + + // Note: hit detection will only apply to elements that report + // -isAccessibilityElement of YES. The framework will continue scanning the + // entire element tree looking for such a hit. + + // We enforce in the framework that no other useful semantics are merged with these nodes. + if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) + return false; + + // If the only flag(s) set are scrolling related AND + // The only flags set are not kIsHidden OR + // The node doesn't have a label, value, or hint OR + // The only actions set are scrolling related actions. + // + // The kIsHidden flag set with any other flag just means this node is now + // hidden but still is a valid target for a11y focus in the tree, e.g. a list + // item that is currently off screen but the a11y navigation needs to know + // about. + return (([self node].flags & ~flutter::kScrollableSemanticsFlags) != 0 && + [self node].flags != static_cast(flutter::SemanticsFlags::kIsHidden)) || + ![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() || + ([self node].actions & ~flutter::kScrollableSemanticsActions) != 0; +} + +- (void)collectRoutes:(NSMutableArray*)edges { + if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) + [edges addObject:self]; + if ([self hasChildren]) { + for (SemanticsObject* child in self.children) { + [child collectRoutes:edges]; + } + } +} + +- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { + if (![self node].HasAction(flutter::SemanticsAction::kCustomAction)) + return NO; + int32_t action_id = action.uid; + std::vector args; + args.push_back(3); // type=int32. + args.push_back(action_id); + args.push_back(action_id >> 8); + args.push_back(action_id >> 16); + args.push_back(action_id >> 24); + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kCustomAction, + std::move(args)); + return YES; +} + +- (NSString*)routeName { + // Returns the first non-null and non-empty semantic label of a child + // with an NamesRoute flag. Otherwise returns nil. + if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) { + NSString* newName = [self accessibilityLabel]; + if (newName != nil && [newName length] > 0) { + return newName; + } + } + if ([self hasChildren]) { + for (SemanticsObject* child in self.children) { + NSString* newName = [child routeName]; + if (newName != nil && [newName length] > 0) { + return newName; + } + } + } + return nil; +} + +- (NSString*)accessibilityLabel { + if (![self isAccessibilityBridgeAlive]) + return nil; + + if ([self node].label.empty()) + return nil; + return @([self node].label.data()); +} + +- (NSString*)accessibilityHint { + if (![self isAccessibilityBridgeAlive]) + return nil; + + if ([self node].hint.empty()) + return nil; + return @([self node].hint.data()); +} + +- (NSString*)accessibilityValue { + if (![self isAccessibilityBridgeAlive]) + return nil; + + if (![self node].value.empty()) { + return @([self node].value.data()); + } + + // FlutterSwitchSemanticsObject should supercede these conditionals. + if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || + [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { + if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) || + [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { + return @"1"; + } else { + return @"0"; + } + } + + return nil; +} + +- (CGRect)accessibilityFrame { + if (![self isAccessibilityBridgeAlive]) + return CGRectMake(0, 0, 0, 0); + + if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { + return [super accessibilityFrame]; + } + return [self globalRect]; +} + +- (CGRect)globalRect { + SkMatrix44 globalTransform = [self node].transform; + for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) { + globalTransform = parent.node.transform * globalTransform; + } + + SkPoint quad[4]; + [self node].rect.toQuad(quad); + for (auto& point : quad) { + SkScalar vector[4] = {point.x(), point.y(), 0, 1}; + globalTransform.mapScalars(vector); + point.set(vector[0] / vector[3], vector[1] / vector[3]); + } + SkRect rect; + rect.setBounds(quad, 4); + + // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in + // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to + // convert. + CGFloat scale = [[[self bridge]->view() window] screen].scale; + auto result = + CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale); + return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge]->view()); +} + +#pragma mark - UIAccessibilityElement protocol + +- (id)accessibilityContainer { + if ([self hasChildren] || [self uid] == kRootNodeId) { + if (_container == nil) + _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self + bridge:[self bridge]]); + return _container.get(); + } + if ([self parent] == nil) { + // This can happen when we have released the accessibility tree but iOS is + // still holding onto our objects. iOS can take some time before it + // realizes that the tree has changed. + return nil; + } + return [[self parent] accessibilityContainer]; +} + +#pragma mark - UIAccessibilityAction overrides + +- (BOOL)accessibilityActivate { + if (![self isAccessibilityBridgeAlive]) + return NO; + if (![self node].HasAction(flutter::SemanticsAction::kTap)) + return NO; + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap); + return YES; +} + +- (void)accessibilityIncrement { + if (![self isAccessibilityBridgeAlive]) + return; + if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) { + [self node].value = [self node].increasedValue; + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease); + } +} + +- (void)accessibilityDecrement { + if (![self isAccessibilityBridgeAlive]) + return; + if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) { + [self node].value = [self node].decreasedValue; + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease); + } +} + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + if (![self isAccessibilityBridgeAlive]) + return NO; + flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction); + if (![self node].HasAction(action)) + return NO; + [self bridge]->DispatchSemanticsAction([self uid], action); + return YES; +} + +- (BOOL)accessibilityPerformEscape { + if (![self isAccessibilityBridgeAlive]) + return NO; + if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) + return NO; + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss); + return YES; +} + +#pragma mark UIAccessibilityFocus overrides + +- (void)accessibilityElementDidBecomeFocused { + if (![self isAccessibilityBridgeAlive]) + return; + if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { + [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen); + } + if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) { + [self bridge]->DispatchSemanticsAction([self uid], + flutter::SemanticsAction::kDidGainAccessibilityFocus); + } +} + +- (void)accessibilityElementDidLoseFocus { + if (![self isAccessibilityBridgeAlive]) + return; + if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) { + [self bridge]->DispatchSemanticsAction([self uid], + flutter::SemanticsAction::kDidLoseAccessibilityFocus); + } +} + +@end + +@implementation FlutterSemanticsObject { +} + +#pragma mark - Override base class designated initializers + +// Method declared as unavailable in the interface +- (instancetype)init { + [self release]; + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +#pragma mark - Designated initializers + +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid { + self = [super initWithBridge:bridge uid:uid]; + return self; +} + +#pragma mark - UIAccessibility overrides + +- (UIAccessibilityTraits)accessibilityTraits { + UIAccessibilityTraits traits = UIAccessibilityTraitNone; + if ([self node].HasAction(flutter::SemanticsAction::kIncrease) || + [self node].HasAction(flutter::SemanticsAction::kDecrease)) { + traits |= UIAccessibilityTraitAdjustable; + } + // FlutterSwitchSemanticsObject should supercede these conditionals. + if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || + [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { + traits |= UIAccessibilityTraitButton; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) { + traits |= UIAccessibilityTraitSelected; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) { + traits |= UIAccessibilityTraitButton; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) && + ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) { + traits |= UIAccessibilityTraitNotEnabled; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { + traits |= UIAccessibilityTraitHeader; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) { + traits |= UIAccessibilityTraitImage; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) { + traits |= UIAccessibilityTraitUpdatesFrequently; + } + if ([self node].HasFlag(flutter::SemanticsFlags::kIsLink)) { + traits |= UIAccessibilityTraitLink; + } + return traits; +} + +@end + +@implementation FlutterPlatformViewSemanticsContainer { + SemanticsObject* _semanticsObject; + UIView* _platformView; +} + +// Method declared as unavailable in the interface +- (instancetype)init { + [self release]; + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithSemanticsObject:(SemanticsObject*)object { + FML_CHECK(object); + // Initialize with the UIView as the container. + // The UIView will not necessarily be accessibility parent for this object. + // The bridge informs the OS of the actual structure via + // `accessibilityContainer` and `accessibilityElementAtIndex`. + if (self = [super initWithAccessibilityContainer:object.bridge->view()]) { + _semanticsObject = object; + flutter::FlutterPlatformViewsController* controller = + object.bridge->GetPlatformViewsController(); + if (controller) { + _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view]; + } + self.accessibilityElements = @[ _semanticsObject, _platformView ]; + } + return self; +} + +- (CGRect)accessibilityFrame { + return _semanticsObject.accessibilityFrame; +} + +- (BOOL)isAccessibilityElement { + return NO; +} + +- (id)accessibilityContainer { + return [_semanticsObject accessibilityContainer]; +} + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + return [_platformView accessibilityScroll:direction]; +} + +@end + +@implementation SemanticsObjectContainer { + SemanticsObject* _semanticsObject; + fml::WeakPtr _bridge; +} + +#pragma mark - initializers + +// Method declared as unavailable in the interface +- (instancetype)init { + [self release]; + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject + bridge:(fml::WeakPtr)bridge { + FML_DCHECK(semanticsObject) << "semanticsObject must be set"; + // Initialize with the UIView as the container. + // The UIView will not necessarily be accessibility parent for this object. + // The bridge informs the OS of the actual structure via + // `accessibilityContainer` and `accessibilityElementAtIndex`. + self = [super initWithAccessibilityContainer:bridge->view()]; + + if (self) { + _semanticsObject = semanticsObject; + _bridge = bridge; + } + + return self; +} + +#pragma mark - UIAccessibilityContainer overrides + +- (NSInteger)accessibilityElementCount { + NSInteger count = [[_semanticsObject children] count] + 1; + return count; +} + +- (nullable id)accessibilityElementAtIndex:(NSInteger)index { + if (index < 0 || index >= [self accessibilityElementCount]) + return nil; + if (index == 0) { + return _semanticsObject; + } + + SemanticsObject* child = [_semanticsObject children][index - 1]; + + // Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer` + if (child.node.IsPlatformViewNode()) { + child.platformViewSemanticsContainer.index = index; + return child.platformViewSemanticsContainer; + } + + if ([child hasChildren]) + return [child accessibilityContainer]; + return child; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element { + if (element == _semanticsObject) + return 0; + + // FlutterPlatformViewSemanticsContainer is always the last element of its parent. + if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) { + return ((FlutterPlatformViewSemanticsContainer*)element).index; + } + + NSArray* children = [_semanticsObject children]; + for (size_t i = 0; i < [children count]; i++) { + SemanticsObject* child = children[i]; + if ((![child hasChildren] && child == element) || + ([child hasChildren] && [child accessibilityContainer] == element)) + return i + 1; + } + return NSNotFound; +} + +#pragma mark - UIAccessibilityElement protocol + +- (BOOL)isAccessibilityElement { + return NO; +} + +- (CGRect)accessibilityFrame { + return [_semanticsObject accessibilityFrame]; +} + +- (id)accessibilityContainer { + if (!_bridge) { + return nil; + } + return ([_semanticsObject uid] == kRootNodeId) + ? _bridge->view() + : [[_semanticsObject parent] accessibilityContainer]; +} + +#pragma mark - UIAccessibilityAction overrides + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + return [_semanticsObject accessibilityScroll:direction]; +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm new file mode 100644 index 0000000000000..b067ee7920fc1 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -0,0 +1,41 @@ +#import +#import + +#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" + +FLUTTER_ASSERT_ARC + +namespace flutter { +namespace { +class MockAccessibilityBridge : public AccessibilityBridgeIos { + public: + MockAccessibilityBridge() { view_ = [[UIView alloc] init]; } + UIView* view() const override { return view_; } + UIView* textInputView() override { return nil; } + void DispatchSemanticsAction(int32_t id, SemanticsAction action) override {} + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + std::vector args) override {} + FlutterPlatformViewsController* GetPlatformViewsController() const override { return nil; } + + private: + UIView* view_; +}; +} // namespace +} // namespace flutter + +@interface SemanticsObjectTest : XCTestCase +@end + +@implementation SemanticsObjectTest + +- (void)testCreate { + fml::WeakPtrFactory factory( + new flutter::MockAccessibilityBridge()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0]; + XCTAssertNotNil(object); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index d6e0bb6446e0c..6b7d61fe1f23e 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -20,143 +20,15 @@ #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" +#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h" #include "third_party/skia/include/core/SkMatrix44.h" #include "third_party/skia/include/core/SkRect.h" -namespace flutter { -class AccessibilityBridge; -} // namespace flutter - -@class FlutterPlatformViewSemanticsContainer; - -/** - * A node in the iOS semantics tree. - */ -@interface SemanticsObject : UIAccessibilityElement - -/** - * The globally unique identifier for this node. - */ -@property(nonatomic, readonly) int32_t uid; - -/** - * The parent of this node in the node tree. Will be nil for the root node and - * during transient state changes. - */ -@property(nonatomic, assign) SemanticsObject* parent; - -/** - * The accessibility bridge that this semantics object is attached to. This - * object may use the bridge to access contextual application information. A weak - * pointer is used because the platform view owns the accessibility bridge. - * If you are referencing this property from an iOS callback, be sure to - * use `isAccessibilityBridgeActive` to protect against the case where this - * node may be orphaned. - */ -@property(nonatomic, readonly) fml::WeakPtr bridge; - -/** - * Due to the fact that VoiceOver may hold onto SemanticObjects even after it shuts down, - * there can be situations where the AccessibilityBridge is shutdown, but the SemanticObject - * will still be alive. If VoiceOver is turned on again, it may try to access this orphaned - * SemanticObject. Methods that are called from the accessiblity framework should use - * this to guard against this case by just returning early if its bridge has been shutdown. - * - * See https://github.com/flutter/flutter/issues/43795 for more information. - */ -- (BOOL)isAccessibilityBridgeAlive; - -/** - * The semantics node used to produce this semantics object. - */ -@property(nonatomic, readonly) flutter::SemanticsNode node; - -/** - * Updates this semantics object using data from the `node` argument. - */ -- (void)setSemanticsNode:(const flutter::SemanticsNode*)node NS_REQUIRES_SUPER; - -/** - * Whether this semantics object has child semantics objects. - */ -@property(nonatomic, readonly) BOOL hasChildren; - -/** - * Direct children of this semantics object. Each child's `parent` property must - * be equal to this object. - */ -@property(nonatomic, strong) NSMutableArray* children; - -/** - * Used if this SemanticsObject is for a platform view. - */ -@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer; - -- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node; - -#pragma mark - Designated initializers - -- (instancetype)init __attribute__((unavailable("Use initWithBridge instead"))); -- (instancetype)initWithBridge:(fml::WeakPtr)bridge - uid:(int32_t)uid NS_DESIGNATED_INITIALIZER; - -@end - -/** - * An implementation of UIAccessibilityCustomAction which also contains the - * Flutter uid. - */ -@interface FlutterCustomAccessibilityAction : UIAccessibilityCustomAction - -/** - * The uid of the action defined by the flutter application. - */ -@property(nonatomic) int32_t uid; - -@end - -/** - * The default implementation of `SemanticsObject` for most accessibility elements - * in the iOS accessibility tree. - * - * Use this implementation for nodes that do not need to be expressed via UIKit-specific - * protocols (it only implements NSObject). - * - * See also: - * * TextInputSemanticsObject, which implements `UITextInput` protocol to expose - * editable text widgets to a11y. - */ -@interface FlutterSemanticsObject : SemanticsObject -@end - -/** - * Designated to act as an accessibility container of a platform view. - * - * This object does not take any accessibility actions on its own, nor has any accessibility - * label/value/trait/hint... on its own. The accessibility data will be handled by the platform - * view. - * - * See also: - * * `SemanticsObject` for the other type of semantics objects. - * * `FlutterSemanticsObject` for default implementation of `SemanticsObject`. - */ -@interface FlutterPlatformViewSemanticsContainer : UIAccessibilityElement - -/** - * The position inside an accessibility container. - */ -@property(nonatomic) NSInteger index; - -- (instancetype)init __attribute__((unavailable("Use initWithAccessibilityContainer: instead"))); - -- (instancetype)initWithSemanticsObject:(SemanticsObject*)object; - -@end - namespace flutter { class PlatformViewIOS; -class AccessibilityBridge final { +class AccessibilityBridge final : public AccessibilityBridgeIos { public: AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view, @@ -165,18 +37,18 @@ class AccessibilityBridge final { void UpdateSemantics(flutter::SemanticsNodeUpdates nodes, flutter::CustomAccessibilityActionUpdates actions); - void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action); + void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action) override; void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, - std::vector args); + std::vector args) override; - UIView* textInputView(); + UIView* textInputView() override; - UIView* view() const { return view_; } + UIView* view() const override { return view_; } fml::WeakPtr GetWeakPtr(); - FlutterPlatformViewsController* GetPlatformViewsController() const { + FlutterPlatformViewsController* GetPlatformViewsController() const override { return platform_views_controller_; }; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index b07a32989f18e..c9c147bdacc55 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -5,692 +5,11 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" #include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h" -#include -#include - -#import - -#include "flutter/fml/logging.h" -#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" -namespace { - -constexpr int32_t kRootNodeId = 0; - -flutter::SemanticsAction GetSemanticsActionForScrollDirection( - UIAccessibilityScrollDirection direction) { - // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the - // direction the scroll bar moves in and SemanticsAction uses the direction the finger - // moves in. However, the horizontal scroll direction matches the SemanticsAction direction. - // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal - // maps directly. - switch (direction) { - case UIAccessibilityScrollDirectionRight: - case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using - // _node.textDirection. - return flutter::SemanticsAction::kScrollRight; - case UIAccessibilityScrollDirectionLeft: - case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using - // _node.textDirection. - return flutter::SemanticsAction::kScrollLeft; - case UIAccessibilityScrollDirectionUp: - return flutter::SemanticsAction::kScrollDown; - case UIAccessibilityScrollDirectionDown: - return flutter::SemanticsAction::kScrollUp; - } - FML_DCHECK(false); // Unreachable - return flutter::SemanticsAction::kScrollUp; -} - -} // namespace - -/// A proxy class for SemanticsObject and UISwitch. For most Accessibility and -/// SemanticsObject methods it delegates to the semantics object, otherwise it -/// sends messages to the UISwitch. -@interface FlutterSwitchSemanticsObject : UISwitch -@end - -@implementation FlutterSwitchSemanticsObject { - SemanticsObject* _semanticsObject; -} - -- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject { - self = [super init]; - if (self) { - _semanticsObject = [semanticsObject retain]; - } - return self; -} - -- (void)dealloc { - [_semanticsObject release]; - [super dealloc]; -} - -- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel { - NSMethodSignature* result = [super methodSignatureForSelector:sel]; - if (!result) { - result = [_semanticsObject methodSignatureForSelector:sel]; - } - return result; -} - -- (void)forwardInvocation:(NSInvocation*)anInvocation { - [anInvocation setTarget:_semanticsObject]; - [anInvocation invoke]; -} - -- (CGRect)accessibilityFrame { - return [_semanticsObject accessibilityFrame]; -} - -- (id)accessibilityContainer { - return [_semanticsObject accessibilityContainer]; -} - -- (NSString*)accessibilityLabel { - return [_semanticsObject accessibilityLabel]; -} - -- (NSString*)accessibilityHint { - return [_semanticsObject accessibilityHint]; -} - -- (NSString*)accessibilityValue { - if ([_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsToggled) || - [_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { - self.on = YES; - } else { - self.on = NO; - } - - if (![_semanticsObject isAccessibilityBridgeAlive]) { - return nil; - } else { - return [super accessibilityValue]; - } -} - -@end // FlutterSwitchSemanticsObject - -@implementation FlutterCustomAccessibilityAction { -} -@end - -/** - * Represents a semantics object that has children and hence has to be presented to the OS as a - * UIAccessibilityContainer. - * - * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an - * object that returns YES for isAccessibilityElement cannot also implement - * UIAccessibilityContainer. - * - * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from - * the framework, such as: - * - * SemanticsObject1 - * SemanticsObject2 - * SemanticsObject3 - * SemanticsObject4 - * - * is translated into the following hierarchy, which is understood by iOS: - * - * SemanticsObjectContainer1 - * SemanticsObject1 - * SemanticsObjectContainer2 - * SemanticsObject2 - * SemanticsObject3 - * SemanticsObject4 - * - * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the - * world (second tree) as follows: We replace each SemanticsObjects that has children with a - * SemanticsObjectContainer, which has the original SemanticsObject and its children as children. - * - * SemanticsObjects have semantic information attached to them which is interpreted by - * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just - * there for structure and they don't provide any semantic information to VoiceOver (they return - * NO for isAccessibilityElement). - */ -@interface SemanticsObjectContainer : UIAccessibilityElement -- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead"))); -- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject - bridge:(fml::WeakPtr)bridge - NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, weak) SemanticsObject* semanticsObject; - -@end - -@implementation SemanticsObject { - fml::scoped_nsobject _container; -} - -#pragma mark - Override base class designated initializers - -// Method declared as unavailable in the interface -- (instancetype)init { - [self release]; - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -#pragma mark - Designated initializers - -- (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid { - FML_DCHECK(bridge) << "bridge must be set"; - FML_DCHECK(uid >= kRootNodeId); - // Initialize with the UIView as the container. - // The UIView will not necessarily be accessibility parent for this object. - // The bridge informs the OS of the actual structure via - // `accessibilityContainer` and `accessibilityElementAtIndex`. - self = [super initWithAccessibilityContainer:bridge->view()]; - - if (self) { - _bridge = bridge; - _uid = uid; - _children = [[NSMutableArray alloc] init]; - } - - return self; -} - -- (void)dealloc { - for (SemanticsObject* child in _children) { - child.parent = nil; - } - [_children removeAllObjects]; - [_children release]; - _parent = nil; - _container.get().semanticsObject = nil; - [_platformViewSemanticsContainer release]; - [super dealloc]; -} - -#pragma mark - Semantic object methods - -- (BOOL)isAccessibilityBridgeAlive { - return [self bridge].get() != nil; -} - -- (void)setSemanticsNode:(const flutter::SemanticsNode*)node { - _node = *node; -} - -/** - * Whether calling `setSemanticsNode:` with `node` would cause a layout change. - */ -- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node { - return [self node].rect != node->rect || [self node].transform != node->transform; -} - -/** - * Whether calling `setSemanticsNode:` with `node` would cause a scroll event. - */ -- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node { - return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) && - [self node].scrollPosition != node->scrollPosition; -} - -- (BOOL)hasChildren { - if (_node.IsPlatformViewNode()) { - return YES; - } - return [self.children count] != 0; -} - -#pragma mark - UIAccessibility overrides - -- (BOOL)isAccessibilityElement { - if (![self isAccessibilityBridgeAlive]) - return false; - - // Note: hit detection will only apply to elements that report - // -isAccessibilityElement of YES. The framework will continue scanning the - // entire element tree looking for such a hit. - - // We enforce in the framework that no other useful semantics are merged with these nodes. - if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) - return false; - - // If the only flag(s) set are scrolling related AND - // The only flags set are not kIsHidden OR - // The node doesn't have a label, value, or hint OR - // The only actions set are scrolling related actions. - // - // The kIsHidden flag set with any other flag just means this node is now - // hidden but still is a valid target for a11y focus in the tree, e.g. a list - // item that is currently off screen but the a11y navigation needs to know - // about. - return (([self node].flags & ~flutter::kScrollableSemanticsFlags) != 0 && - [self node].flags != static_cast(flutter::SemanticsFlags::kIsHidden)) || - ![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() || - ([self node].actions & ~flutter::kScrollableSemanticsActions) != 0; -} - -- (void)collectRoutes:(NSMutableArray*)edges { - if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) - [edges addObject:self]; - if ([self hasChildren]) { - for (SemanticsObject* child in self.children) { - [child collectRoutes:edges]; - } - } -} - -- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { - if (![self node].HasAction(flutter::SemanticsAction::kCustomAction)) - return NO; - int32_t action_id = action.uid; - std::vector args; - args.push_back(3); // type=int32. - args.push_back(action_id); - args.push_back(action_id >> 8); - args.push_back(action_id >> 16); - args.push_back(action_id >> 24); - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kCustomAction, - std::move(args)); - return YES; -} - -- (NSString*)routeName { - // Returns the first non-null and non-empty semantic label of a child - // with an NamesRoute flag. Otherwise returns nil. - if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) { - NSString* newName = [self accessibilityLabel]; - if (newName != nil && [newName length] > 0) { - return newName; - } - } - if ([self hasChildren]) { - for (SemanticsObject* child in self.children) { - NSString* newName = [child routeName]; - if (newName != nil && [newName length] > 0) { - return newName; - } - } - } - return nil; -} - -- (NSString*)accessibilityLabel { - if (![self isAccessibilityBridgeAlive]) - return nil; - - if ([self node].label.empty()) - return nil; - return @([self node].label.data()); -} - -- (NSString*)accessibilityHint { - if (![self isAccessibilityBridgeAlive]) - return nil; - - if ([self node].hint.empty()) - return nil; - return @([self node].hint.data()); -} - -- (NSString*)accessibilityValue { - if (![self isAccessibilityBridgeAlive]) - return nil; - - if (![self node].value.empty()) { - return @([self node].value.data()); - } - - // FlutterSwitchSemanticsObject should supercede these conditionals. - if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || - [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { - if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) || - [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { - return @"1"; - } else { - return @"0"; - } - } - - return nil; -} - -- (CGRect)accessibilityFrame { - if (![self isAccessibilityBridgeAlive]) - return CGRectMake(0, 0, 0, 0); - - if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { - return [super accessibilityFrame]; - } - return [self globalRect]; -} - -- (CGRect)globalRect { - SkMatrix44 globalTransform = [self node].transform; - for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) { - globalTransform = parent.node.transform * globalTransform; - } - - SkPoint quad[4]; - [self node].rect.toQuad(quad); - for (auto& point : quad) { - SkScalar vector[4] = {point.x(), point.y(), 0, 1}; - globalTransform.mapScalars(vector); - point.set(vector[0] / vector[3], vector[1] / vector[3]); - } - SkRect rect; - rect.setBounds(quad, 4); - - // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in - // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to - // convert. - CGFloat scale = [[[self bridge]->view() window] screen].scale; - auto result = - CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale); - return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge] -> view()); -} - -#pragma mark - UIAccessibilityElement protocol - -- (id)accessibilityContainer { - if ([self hasChildren] || [self uid] == kRootNodeId) { - if (_container == nil) - _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self - bridge:[self bridge]]); - return _container.get(); - } - if ([self parent] == nil) { - // This can happen when we have released the accessibility tree but iOS is - // still holding onto our objects. iOS can take some time before it - // realizes that the tree has changed. - return nil; - } - return [[self parent] accessibilityContainer]; -} - -#pragma mark - UIAccessibilityAction overrides - -- (BOOL)accessibilityActivate { - if (![self isAccessibilityBridgeAlive]) - return NO; - if (![self node].HasAction(flutter::SemanticsAction::kTap)) - return NO; - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap); - return YES; -} - -- (void)accessibilityIncrement { - if (![self isAccessibilityBridgeAlive]) - return; - if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) { - [self node].value = [self node].increasedValue; - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease); - } -} - -- (void)accessibilityDecrement { - if (![self isAccessibilityBridgeAlive]) - return; - if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) { - [self node].value = [self node].decreasedValue; - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease); - } -} - -- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { - if (![self isAccessibilityBridgeAlive]) - return NO; - flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction); - if (![self node].HasAction(action)) - return NO; - [self bridge] -> DispatchSemanticsAction([self uid], action); - return YES; -} - -- (BOOL)accessibilityPerformEscape { - if (![self isAccessibilityBridgeAlive]) - return NO; - if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) - return NO; - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss); - return YES; -} - -#pragma mark UIAccessibilityFocus overrides - -- (void)accessibilityElementDidBecomeFocused { - if (![self isAccessibilityBridgeAlive]) - return; - if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { - [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen); - } - if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) { - [self bridge] -> DispatchSemanticsAction([self uid], - flutter::SemanticsAction::kDidGainAccessibilityFocus); - } -} - -- (void)accessibilityElementDidLoseFocus { - if (![self isAccessibilityBridgeAlive]) - return; - if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) { - [self bridge] -> DispatchSemanticsAction([self uid], - flutter::SemanticsAction::kDidLoseAccessibilityFocus); - } -} - -@end - -@implementation FlutterSemanticsObject { -} - -#pragma mark - Override base class designated initializers - -// Method declared as unavailable in the interface -- (instancetype)init { - [self release]; - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -#pragma mark - Designated initializers - -- (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid { - self = [super initWithBridge:bridge uid:uid]; - return self; -} - -#pragma mark - UIAccessibility overrides - -- (UIAccessibilityTraits)accessibilityTraits { - UIAccessibilityTraits traits = UIAccessibilityTraitNone; - if ([self node].HasAction(flutter::SemanticsAction::kIncrease) || - [self node].HasAction(flutter::SemanticsAction::kDecrease)) { - traits |= UIAccessibilityTraitAdjustable; - } - // FlutterSwitchSemanticsObject should supercede these conditionals. - if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || - [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { - traits |= UIAccessibilityTraitButton; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) { - traits |= UIAccessibilityTraitSelected; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) { - traits |= UIAccessibilityTraitButton; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) && - ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) { - traits |= UIAccessibilityTraitNotEnabled; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { - traits |= UIAccessibilityTraitHeader; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) { - traits |= UIAccessibilityTraitImage; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) { - traits |= UIAccessibilityTraitUpdatesFrequently; - } - if ([self node].HasFlag(flutter::SemanticsFlags::kIsLink)) { - traits |= UIAccessibilityTraitLink; - } - return traits; -} - -@end - -@implementation FlutterPlatformViewSemanticsContainer { - SemanticsObject* _semanticsObject; - UIView* _platformView; -} - -// Method declared as unavailable in the interface -- (instancetype)init { - [self release]; - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -- (instancetype)initWithSemanticsObject:(SemanticsObject*)object { - FML_CHECK(object); - // Initialize with the UIView as the container. - // The UIView will not necessarily be accessibility parent for this object. - // The bridge informs the OS of the actual structure via - // `accessibilityContainer` and `accessibilityElementAtIndex`. - if (self = [super initWithAccessibilityContainer:object.bridge->view()]) { - _semanticsObject = object; - flutter::FlutterPlatformViewsController* controller = - object.bridge->GetPlatformViewsController(); - if (controller) { - _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view]; - } - self.accessibilityElements = @[ _semanticsObject, _platformView ]; - } - return self; -} - -- (CGRect)accessibilityFrame { - return _semanticsObject.accessibilityFrame; -} - -- (BOOL)isAccessibilityElement { - return NO; -} - -- (id)accessibilityContainer { - return [_semanticsObject accessibilityContainer]; -} - -- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { - return [_platformView accessibilityScroll:direction]; -} - -@end - -@implementation SemanticsObjectContainer { - SemanticsObject* _semanticsObject; - fml::WeakPtr _bridge; -} - -#pragma mark - initializers - -// Method declared as unavailable in the interface -- (instancetype)init { - [self release]; - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject - bridge:(fml::WeakPtr)bridge { - FML_DCHECK(semanticsObject) << "semanticsObject must be set"; - // Initialize with the UIView as the container. - // The UIView will not necessarily be accessibility parent for this object. - // The bridge informs the OS of the actual structure via - // `accessibilityContainer` and `accessibilityElementAtIndex`. - self = [super initWithAccessibilityContainer:bridge->view()]; - - if (self) { - _semanticsObject = semanticsObject; - _bridge = bridge; - } - - return self; -} - -#pragma mark - UIAccessibilityContainer overrides - -- (NSInteger)accessibilityElementCount { - NSInteger count = [[_semanticsObject children] count] + 1; - return count; -} - -- (nullable id)accessibilityElementAtIndex:(NSInteger)index { - if (index < 0 || index >= [self accessibilityElementCount]) - return nil; - if (index == 0) { - return _semanticsObject; - } - - SemanticsObject* child = [_semanticsObject children][index - 1]; - - // Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer` - if (child.node.IsPlatformViewNode()) { - child.platformViewSemanticsContainer.index = index; - return child.platformViewSemanticsContainer; - } - - if ([child hasChildren]) - return [child accessibilityContainer]; - return child; -} - -- (NSInteger)indexOfAccessibilityElement:(id)element { - if (element == _semanticsObject) - return 0; - - // FlutterPlatformViewSemanticsContainer is always the last element of its parent. - if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) { - return ((FlutterPlatformViewSemanticsContainer*)element).index; - } - - NSMutableArray* children = [_semanticsObject children]; - for (size_t i = 0; i < [children count]; i++) { - SemanticsObject* child = children[i]; - if ((![child hasChildren] && child == element) || - ([child hasChildren] && [child accessibilityContainer] == element)) - return i + 1; - } - return NSNotFound; -} - -#pragma mark - UIAccessibilityElement protocol - -- (BOOL)isAccessibilityElement { - return NO; -} - -- (CGRect)accessibilityFrame { - return [_semanticsObject accessibilityFrame]; -} - -- (id)accessibilityContainer { - if (!_bridge) { - return nil; - } - return ([_semanticsObject uid] == kRootNodeId) - ? _bridge->view() - : [[_semanticsObject parent] accessibilityContainer]; -} - -#pragma mark - UIAccessibilityAction overrides - -- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { - return [_semanticsObject accessibilityScroll:direction]; -} - -@end +#pragma GCC diagnostic error "-Wundeclared-selector" -#pragma mark - AccessibilityBridge impl +FLUTTER_ASSERT_NOT_ARC namespace flutter { @@ -741,7 +60,6 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease]; for (NSUInteger i = 0; i < newChildCount; ++i) { SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes); - child.parent = object; [newChildren addObject:child]; } object.children = newChildren; @@ -847,10 +165,8 @@ static void ReplaceSemanticsObject(SemanticsObject* oldObject, assert(oldObject.node.id == newObject.node.id); NSNumber* nodeId = @(oldObject.node.id); NSUInteger positionInChildlist = [oldObject.parent.children indexOfObject:oldObject]; - SemanticsObject* parent = oldObject.parent; [objects removeObjectForKey:nodeId]; - newObject.parent = parent; - [newObject.parent.children replaceObjectAtIndex:positionInChildlist withObject:newObject]; + [oldObject.parent replaceChildAtIndex:positionInChildlist withChild:newObject]; objects[nodeId] = newObject; } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h new file mode 100644 index 0000000000000..c2546ac7c3a2c --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h @@ -0,0 +1,32 @@ +// 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 SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_IOS_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_IOS_H_ + +#include + +#include "flutter/lib/ui/semantics/semantics_node.h" + +@class UIView; + +namespace flutter { +class FlutterPlatformViewsController; + +/// Interface that represents an accessibility bridge for iOS. +class AccessibilityBridgeIos { + public: + virtual ~AccessibilityBridgeIos() = default; + virtual UIView* view() const = 0; + virtual UIView* textInputView() = 0; + virtual void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action) = 0; + virtual void DispatchSemanticsAction(int32_t id, + flutter::SemanticsAction action, + std::vector args) = 0; + virtual FlutterPlatformViewsController* GetPlatformViewsController() const = 0; +}; + +} // namespace flutter + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_IOS_H_ diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm b/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm index 0bf0a6963606f..f39f19174d562 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm @@ -7,6 +7,8 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" #include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h" +static const UIAccessibilityTraits UIAccessibilityTraitUndocumentedEmptyLine = 0x800000000000; + @implementation FlutterInactiveTextInput { } @@ -175,7 +177,8 @@ @implementation TextInputSemanticsObject { FlutterInactiveTextInput* _inactive_text_input; } -- (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid { +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid { self = [super initWithBridge:bridge uid:uid]; if (self) { @@ -198,7 +201,7 @@ - (void)setSemanticsNode:(const flutter::SemanticsNode*)node { if ([self node].HasFlag(flutter::SemanticsFlags::kIsFocused)) { // The text input view must have a non-trivial size for the accessibility // system to send text editing events. - [self bridge] -> textInputView().frame = CGRectMake(0.0, 0.0, 1.0, 1.0); + [self bridge]->textInputView().frame = CGRectMake(0.0, 0.0, 1.0, 1.0); } } @@ -214,7 +217,7 @@ - (void)setSemanticsNode:(const flutter::SemanticsNode*)node { */ - (UIView*)textInputSurrogate { if ([self node].HasFlag(flutter::SemanticsFlags::kIsFocused)) { - return [self bridge] -> textInputView(); + return [self bridge]->textInputView(); } else { return _inactive_text_input; } @@ -284,8 +287,13 @@ - (UIAccessibilityTraits)accessibilityTraits { // Adding UIAccessibilityTraitKeyboardKey to the trait list so that iOS treats it like // a keyboard entry control, thus adding support for text editing features, such as // pinch to select text, and up/down fling to move cursor. - return [super accessibilityTraits] | [self textInputSurrogate].accessibilityTraits | - UIAccessibilityTraitKeyboardKey; + UIAccessibilityTraits results = [super accessibilityTraits] | + [self textInputSurrogate].accessibilityTraits | + UIAccessibilityTraitKeyboardKey; + // We remove an undocumented flag to get rid of a bug where single-tapping + // a text input field incorrectly says "empty line". + // See also: https://github.com/flutter/flutter/issues/52487 + return results & (~UIAccessibilityTraitUndocumentedEmptyLine); } #pragma mark - UITextInput overrides diff --git a/shell/platform/darwin/ios/ios_context.h b/shell/platform/darwin/ios/ios_context.h index 6615821905807..37fccd4b3de2e 100644 --- a/shell/platform/darwin/ios/ios_context.h +++ b/shell/platform/darwin/ios/ios_context.h @@ -7,8 +7,10 @@ #include +#include "flutter/flow/texture.h" #include "flutter/fml/macros.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterTexture.h" #include "flutter/shell/platform/darwin/ios/rendering_api_selection.h" #include "third_party/skia/include/gpu/GrContext.h" @@ -119,6 +121,20 @@ class IOSContext { /// virtual bool ClearCurrent() = 0; + //---------------------------------------------------------------------------- + /// @brief Creates an external texture proxy of the appropriate client + /// rendering API. + /// + /// @param[in] texture_id The texture identifier + /// @param[in] texture The texture + /// + /// @return The texture proxy if the rendering backend supports embedder + /// provided external textures. + /// + virtual std::unique_ptr CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) = 0; + protected: IOSContext(); diff --git a/shell/platform/darwin/ios/ios_context_gl.h b/shell/platform/darwin/ios/ios_context_gl.h index fd8885a42395a..56308831b954c 100644 --- a/shell/platform/darwin/ios/ios_context_gl.h +++ b/shell/platform/darwin/ios/ios_context_gl.h @@ -40,6 +40,11 @@ class IOSContextGL final : public IOSContext { // |IOSContext| bool ResourceMakeCurrent() override; + // |IOSContext| + std::unique_ptr CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) override; + FML_DISALLOW_COPY_AND_ASSIGN(IOSContextGL); }; diff --git a/shell/platform/darwin/ios/ios_context_gl.mm b/shell/platform/darwin/ios/ios_context_gl.mm index 1396198338193..2a5a32bf8c5cb 100644 --- a/shell/platform/darwin/ios/ios_context_gl.mm +++ b/shell/platform/darwin/ios/ios_context_gl.mm @@ -8,6 +8,7 @@ #include "flutter/shell/common/shell_io_manager.h" #include "flutter/shell/gpu/gpu_surface_gl_delegate.h" +#include "flutter/shell/platform/darwin/ios/ios_external_texture_gl.h" namespace flutter { @@ -58,4 +59,11 @@ return [EAGLContext setCurrentContext:nil]; } +// |IOSContext| +std::unique_ptr IOSContextGL::CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) { + return std::make_unique(texture_id, std::move(texture)); +} + } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_context_metal.h b/shell/platform/darwin/ios/ios_context_metal.h index f98b11f72d9cc..194b3fc4a0084 100644 --- a/shell/platform/darwin/ios/ios_context_metal.h +++ b/shell/platform/darwin/ios/ios_context_metal.h @@ -8,6 +8,7 @@ #include #include "flutter/fml/macros.h" +#include "flutter/fml/platform/darwin/cf_utils.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/shell/platform/darwin/ios/ios_context.h" #include "third_party/skia/include/gpu/GrContext.h" @@ -35,6 +36,7 @@ class IOSContextMetal final : public IOSContext { fml::scoped_nsprotocol> main_queue_; sk_sp main_context_; sk_sp resource_context_; + fml::CFRef texture_cache_; bool is_valid_ = false; // |IOSContext| @@ -49,6 +51,11 @@ class IOSContextMetal final : public IOSContext { // |IOSContext| bool ClearCurrent() override; + // |IOSContext| + std::unique_ptr CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) override; + FML_DISALLOW_COPY_AND_ASSIGN(IOSContextMetal); }; diff --git a/shell/platform/darwin/ios/ios_context_metal.mm b/shell/platform/darwin/ios/ios_context_metal.mm index 8b9cbda33455b..ea5de5988c3b5 100644 --- a/shell/platform/darwin/ios/ios_context_metal.mm +++ b/shell/platform/darwin/ios/ios_context_metal.mm @@ -5,34 +5,64 @@ #include "flutter/shell/platform/darwin/ios/ios_context_metal.h" #include "flutter/fml/logging.h" +#include "flutter/shell/common/persistent_cache.h" +#include "flutter/shell/platform/darwin/ios/ios_external_texture_metal.h" +#include "third_party/skia/include/gpu/GrContextOptions.h" namespace flutter { +static GrContextOptions CreateMetalGrContextOptions() { + GrContextOptions options = {}; + if (PersistentCache::cache_sksl()) { + options.fShaderCacheStrategy = GrContextOptions::ShaderCacheStrategy::kSkSL; + } + PersistentCache::MarkStrategySet(); + options.fPersistentCache = PersistentCache::GetCacheForProcess(); + return options; +} + IOSContextMetal::IOSContextMetal() { device_.reset([MTLCreateSystemDefaultDevice() retain]); if (!device_) { - FML_LOG(ERROR) << "Could not acquire Metal device."; + FML_DLOG(ERROR) << "Could not acquire Metal device."; return; } main_queue_.reset([device_ newCommandQueue]); if (!main_queue_) { - FML_LOG(ERROR) << "Could not create Metal command queue."; + FML_DLOG(ERROR) << "Could not create Metal command queue."; return; } [main_queue_ setLabel:@"Flutter Main Queue"]; + const auto& context_options = CreateMetalGrContextOptions(); + // Skia expect arguments to `MakeMetal` transfer ownership of the reference in for release later // when the GrContext is collected. - main_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]); - resource_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]); + main_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain], context_options); + resource_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain], context_options); if (!main_context_ || !resource_context_) { - FML_LOG(ERROR) << "Could not create Skia Metal contexts."; + FML_DLOG(ERROR) << "Could not create Skia Metal contexts."; + return; + } + + resource_context_->setResourceCacheLimits(0u, 0u); + + CVMetalTextureCacheRef texture_cache_raw = NULL; + auto cv_return = CVMetalTextureCacheCreate(kCFAllocatorDefault, // allocator + NULL, // cache attributes (NULL default) + device_.get(), // metal device + NULL, // texture attributes (NULL default) + &texture_cache_raw // [out] cache + ); + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture cache."; return; } + texture_cache_.Reset(texture_cache_raw); is_valid_ = false; } @@ -83,4 +113,11 @@ return true; } +// |IOSContext| +std::unique_ptr IOSContextMetal::CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) { + return std::make_unique(texture_id, texture_cache_, std::move(texture)); +} + } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_context_software.h b/shell/platform/darwin/ios/ios_context_software.h index 558f39a637f97..0bd11c4538b43 100644 --- a/shell/platform/darwin/ios/ios_context_software.h +++ b/shell/platform/darwin/ios/ios_context_software.h @@ -29,6 +29,11 @@ class IOSContextSoftware final : public IOSContext { // |IOSContext| bool ClearCurrent() override; + // |IOSContext| + std::unique_ptr CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) override; + private: FML_DISALLOW_COPY_AND_ASSIGN(IOSContextSoftware); }; diff --git a/shell/platform/darwin/ios/ios_context_software.mm b/shell/platform/darwin/ios/ios_context_software.mm index 8bb029d5792d3..ed11bd542404d 100644 --- a/shell/platform/darwin/ios/ios_context_software.mm +++ b/shell/platform/darwin/ios/ios_context_software.mm @@ -31,4 +31,18 @@ return false; } +// |IOSContext| +std::unique_ptr IOSContextSoftware::CreateExternalTexture( + int64_t texture_id, + fml::scoped_nsobject> texture) { + // Don't use FML for logging as it will contain engine specific details. This is a user facing + // message. + NSLog(@"Flutter: Attempted to composite external texture sources using the software backend. " + @"This backend is only used on simulators. This feature is only available on actual " + @"devices where OpenGL or Metal is used for rendering."); + + // Not supported in this backend. + return nullptr; +} + } // namespace flutter diff --git a/shell/platform/darwin/ios/ios_external_texture_gl.mm b/shell/platform/darwin/ios/ios_external_texture_gl.mm index 19cb324ca8c62..697992d1b7cd8 100644 --- a/shell/platform/darwin/ios/ios_external_texture_gl.mm +++ b/shell/platform/darwin/ios/ios_external_texture_gl.mm @@ -56,7 +56,7 @@ bool IOSExternalTextureGL::NeedUpdateTexture(bool freeze) { // Update texture if `texture_ref_` is reset to `nullptr` when GrContext - // is destroied or new frame is ready. + // is destroyed or new frame is ready. return (!freeze && new_frame_ready_) || !texture_ref_; } diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.h b/shell/platform/darwin/ios/ios_external_texture_metal.h new file mode 100644 index 0000000000000..efedc9f19f4ef --- /dev/null +++ b/shell/platform/darwin/ios/ios_external_texture_metal.h @@ -0,0 +1,58 @@ +// 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_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_TEXTURE_METAL_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_TEXTURE_METAL_H_ + +#include + +#import + +#include "flutter/flow/texture.h" +#include "flutter/fml/macros.h" +#include "flutter/fml/platform/darwin/cf_utils.h" +#include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterTexture.h" +#include "third_party/skia/include/core/SkImage.h" + +namespace flutter { + +class IOSExternalTextureMetal final : public Texture { + public: + IOSExternalTextureMetal(int64_t texture_id, + fml::CFRef texture_cache, + fml::scoped_nsobject> external_texture); + + // |Texture| + ~IOSExternalTextureMetal(); + + private: + fml::CFRef texture_cache_; + fml::scoped_nsobject> external_texture_; + std::atomic_bool texture_frame_available_; + sk_sp external_image_; + + // |Texture| + void Paint(SkCanvas& canvas, const SkRect& bounds, bool freeze, GrContext* context) override; + + // |Texture| + void OnGrContextCreated() override; + + // |Texture| + void OnGrContextDestroyed() override; + + // |Texture| + void MarkNewFrameAvailable() override; + + // |Texture| + void OnTextureUnregistered() override; + + sk_sp WrapExternalPixelBuffer(GrContext* context) const; + + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalTextureMetal); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_IOS_EXTERNAL_TEXTURE_METAL_H_ diff --git a/shell/platform/darwin/ios/ios_external_texture_metal.mm b/shell/platform/darwin/ios/ios_external_texture_metal.mm new file mode 100644 index 0000000000000..e9871327123b0 --- /dev/null +++ b/shell/platform/darwin/ios/ios_external_texture_metal.mm @@ -0,0 +1,145 @@ +// 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/shell/platform/darwin/ios/ios_external_texture_metal.h" + +#include "flutter/fml/logging.h" +#include "third_party/skia/include/gpu/GrBackendSurface.h" +#include "third_party/skia/include/gpu/mtl/GrMtlTypes.h" + +namespace flutter { + +IOSExternalTextureMetal::IOSExternalTextureMetal( + int64_t texture_id, + fml::CFRef texture_cache, + fml::scoped_nsobject> external_texture) + : Texture(texture_id), + texture_cache_(std::move(texture_cache)), + external_texture_(std::move(external_texture)) { + FML_DCHECK(texture_cache_); + FML_DCHECK(external_texture_); +} + +IOSExternalTextureMetal::~IOSExternalTextureMetal() = default; + +void IOSExternalTextureMetal::Paint(SkCanvas& canvas, + const SkRect& bounds, + bool freeze, + GrContext* context) { + const bool needs_updated_texture = (!freeze && texture_frame_available_) || !external_image_; + + if (needs_updated_texture) { + // If the application told us there was a texture frame available but did not provide one when + // asked for it, reuse the previous texture but make sure to ask again the next time around. + if (auto wrapped_texture = WrapExternalPixelBuffer(context)) { + external_image_ = wrapped_texture; + texture_frame_available_ = false; + } + } + + if (external_image_) { + canvas.drawImageRect(external_image_, // image + external_image_->bounds(), // source rect + bounds, // destination rect + nullptr, // paint + SkCanvas::SrcRectConstraint::kFast_SrcRectConstraint // constraint + ); + } +} + +sk_sp IOSExternalTextureMetal::WrapExternalPixelBuffer(GrContext* context) const { + auto pixel_buffer = fml::CFRef([external_texture_ copyPixelBuffer]); + if (!pixel_buffer) { + return nullptr; + } + + auto texture_size = + SkISize::Make(CVPixelBufferGetWidth(pixel_buffer), CVPixelBufferGetHeight(pixel_buffer)); + + CVMetalTextureRef metal_texture_raw = NULL; + auto cv_return = + CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, // allocator + texture_cache_, // texture cache + pixel_buffer, // source image + NULL, // texture attributes + MTLPixelFormatBGRA8Unorm, // pixel format + texture_size.width(), // width + texture_size.height(), // height + 0u, // plane index + &metal_texture_raw // [out] texture + ); + + if (cv_return != kCVReturnSuccess) { + FML_DLOG(ERROR) << "Could not create Metal texture from pixel buffer: CVReturn " << cv_return; + return nullptr; + } + + fml::CFRef metal_texture(metal_texture_raw); + + GrMtlTextureInfo skia_texture_info; + skia_texture_info.fTexture = sk_cf_obj{ + [reinterpret_cast(CVMetalTextureGetTexture(metal_texture)) retain]}; + + GrBackendTexture skia_backend_texture(texture_size.width(), // width + texture_size.height(), // height + GrMipMapped ::kNo, // mip-mapped + skia_texture_info // texture info + ); + + struct ImageCaptures { + fml::CFRef buffer; + fml::CFRef texture; + }; + + auto captures = std::make_unique(); + captures->buffer = std::move(pixel_buffer); + captures->texture = std::move(metal_texture); + + SkImage::TextureReleaseProc release_proc = [](SkImage::ReleaseContext release_context) { + auto captures = reinterpret_cast(release_context); + delete captures; + }; + + auto image = SkImage::MakeFromTexture(context, // context + skia_backend_texture, // backend texture + kTopLeft_GrSurfaceOrigin, // origin + kBGRA_8888_SkColorType, // color type + kPremul_SkAlphaType, // alpha type + nullptr, // color space + release_proc, // release proc + captures.release() // release context + + ); + + if (!image) { + FML_DLOG(ERROR) << "Could not wrap Metal texture as a Skia image."; + } + + return image; +} + +void IOSExternalTextureMetal::OnGrContextCreated() { + // External images in this backend have no thread affinity and are not tied to the context in any + // way. Instead, they are tied to the Metal device which is associated with the cache already and + // is consistent throughout the shell run. +} + +void IOSExternalTextureMetal::OnGrContextDestroyed() { + external_image_.reset(); + CVMetalTextureCacheFlush(texture_cache_, // cache + 0 // options (must be zero) + ); +} + +void IOSExternalTextureMetal::MarkNewFrameAvailable() { + texture_frame_available_ = true; +} + +void IOSExternalTextureMetal::OnTextureUnregistered() { + if ([external_texture_ respondsToSelector:@selector(onTextureUnregistered:)]) { + [external_texture_ onTextureUnregistered:external_texture_]; + } +} + +} // namespace flutter diff --git a/shell/platform/darwin/ios/ios_render_target_gl.mm b/shell/platform/darwin/ios/ios_render_target_gl.mm index 34b57a787d843..7f58a7d7fa24b 100644 --- a/shell/platform/darwin/ios/ios_render_target_gl.mm +++ b/shell/platform/darwin/ios/ios_render_target_gl.mm @@ -70,7 +70,11 @@ glDeleteRenderbuffers(1, &colorbuffer_); FML_DCHECK(glGetError() == GL_NO_ERROR); - [EAGLContext setCurrentContext:context]; + if (context == context_.get()) { + [EAGLContext setCurrentContext:nil]; + } else { + [EAGLContext setCurrentContext:context]; + } } // |IOSRenderTarget| diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index 58233d8a2f656..eae39031d77ce 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -68,7 +68,8 @@ class IOSSurface : public ExternalViewEmbedder { std::unique_ptr params) override; // |ExternalViewEmbedder| - PostPrerollResult PostPrerollAction(fml::RefPtr gpu_thread_merger) override; + PostPrerollResult PostPrerollAction( + fml::RefPtr raster_thread_merger) override; // |ExternalViewEmbedder| std::vector GetCurrentCanvases() override; @@ -77,7 +78,13 @@ class IOSSurface : public ExternalViewEmbedder { SkCanvas* CompositeEmbeddedView(int view_id) override; // |ExternalViewEmbedder| - bool SubmitFrame(GrContext* context) override; + bool SubmitFrame(GrContext* context, SkCanvas* background_canvas) override; + + // |ExternalViewEmbedder| + void FinishFrame() override; + + // |ExternalViewEmbedder| + void EndFrame(fml::RefPtr raster_thread_merger) override; public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index fe85c77f12c2d..5f2725273e0dd 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -90,7 +90,12 @@ bool IsIosEmbeddedViewsPreviewEnabled() { platform_views_controller_->CancelFrame(); // Committing the current transaction as |BeginFrame| will create a nested // CATransaction otherwise. - [CATransaction commit]; + if ([[NSThread currentThread] isMainThread]) { + // The only time we need to commit the `CATranscation` is when + // there are platform views in the scene, which has to be run on the + // main thread. + [CATransaction commit]; + } } // |ExternalViewEmbedder| @@ -98,7 +103,12 @@ bool IsIosEmbeddedViewsPreviewEnabled() { TRACE_EVENT0("flutter", "IOSSurface::BeginFrame"); FML_CHECK(platform_views_controller_ != nullptr); platform_views_controller_->SetFrameSize(frame_size); - [CATransaction begin]; + if ([[NSThread currentThread] isMainThread]) { + // The only time we need to commit the `CATranscation` is when + // there are platform views in the scene, which has to be run on the + // main thread. + [CATransaction begin]; + } } // |ExternalViewEmbedder| @@ -112,10 +122,10 @@ bool IsIosEmbeddedViewsPreviewEnabled() { // |ExternalViewEmbedder| PostPrerollResult IOSSurface::PostPrerollAction( - fml::RefPtr gpu_thread_merger) { + fml::RefPtr raster_thread_merger) { TRACE_EVENT0("flutter", "IOSSurface::PostPrerollAction"); FML_CHECK(platform_views_controller_ != nullptr); - return platform_views_controller_->PostPrerollAction(gpu_thread_merger); + return platform_views_controller_->PostPrerollAction(raster_thread_merger); } // |ExternalViewEmbedder| @@ -132,12 +142,30 @@ bool IsIosEmbeddedViewsPreviewEnabled() { } // |ExternalViewEmbedder| -bool IOSSurface::SubmitFrame(GrContext* context) { +bool IOSSurface::SubmitFrame(GrContext* context, SkCanvas* background_canvas) { TRACE_EVENT0("flutter", "IOSSurface::SubmitFrame"); FML_CHECK(platform_views_controller_ != nullptr); - bool submitted = platform_views_controller_->SubmitFrame(std::move(context), ios_context_); - [CATransaction commit]; + bool submitted = + platform_views_controller_->SubmitFrame(std::move(context), ios_context_, background_canvas); return submitted; } +// |ExternalViewEmbedder| +void IOSSurface::EndFrame(fml::RefPtr raster_thread_merger) { + TRACE_EVENT0("flutter", "IOSSurface::EndFrame"); + FML_CHECK(platform_views_controller_ != nullptr); + return platform_views_controller_->EndFrame(raster_thread_merger); +} + +// |ExternalViewEmbedder| +void IOSSurface::FinishFrame() { + TRACE_EVENT0("flutter", "IOSSurface::DidSubmitFrame"); + if (![[NSThread currentThread] isMainThread]) { + return; + } + // The only time we need to commit the `CATranscation` is when + // there are platform views in the scene, which has to be run on the + // main thread. + [CATransaction commit]; +} } // namespace flutter diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index 4bd76980f8879..95f6fc0ecfacf 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -66,14 +66,32 @@ class PlatformViewIOS final : public PlatformView { id observer_; }; + /// Smart pointer that guarentees we communicate clearing Accessibility + /// information to Dart. + class AccessibilityBridgePtr { + public: + AccessibilityBridgePtr(const std::function& set_semantics_enabled); + AccessibilityBridgePtr(const std::function& set_semantics_enabled, + AccessibilityBridge* bridge); + ~AccessibilityBridgePtr(); + explicit operator bool() const noexcept { return static_cast(accessibility_bridge_); } + AccessibilityBridge* operator->() const noexcept { return accessibility_bridge_.get(); } + void reset(AccessibilityBridge* bridge = nullptr); + + private: + FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridgePtr); + std::unique_ptr accessibility_bridge_; + std::function set_semantics_enabled_; + }; + fml::WeakPtr owner_controller_; // Since the `ios_surface_` is created on the platform thread but - // used on the GPU thread we need to protect it with a mutex. + // used on the raster thread we need to protect it with a mutex. std::mutex ios_surface_mutex_; std::unique_ptr ios_surface_; std::shared_ptr ios_context_; PlatformMessageRouter platform_message_router_; - std::unique_ptr accessibility_bridge_; + AccessibilityBridgePtr accessibility_bridge_; fml::scoped_nsprotocol text_input_plugin_; fml::closure firstFrameCallback_; ScopedObserver dealloc_view_controller_observer_; diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index 09d19b4e61699..772d5775bab6f 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -4,26 +4,52 @@ #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#import - #include #include "flutter/common/task_runners.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/fml/trace_event.h" #include "flutter/shell/common/shell_io_manager.h" -#include "flutter/shell/gpu/gpu_surface_gl_delegate.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #include "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" -#include "flutter/shell/platform/darwin/ios/ios_external_texture_gl.h" namespace flutter { +PlatformViewIOS::AccessibilityBridgePtr::AccessibilityBridgePtr( + const std::function& set_semantics_enabled) + : AccessibilityBridgePtr(set_semantics_enabled, nullptr) {} + +PlatformViewIOS::AccessibilityBridgePtr::AccessibilityBridgePtr( + const std::function& set_semantics_enabled, + AccessibilityBridge* bridge) + : accessibility_bridge_(bridge), set_semantics_enabled_(set_semantics_enabled) { + if (bridge) { + set_semantics_enabled_(true); + } +} + +PlatformViewIOS::AccessibilityBridgePtr::~AccessibilityBridgePtr() { + if (accessibility_bridge_) { + set_semantics_enabled_(false); + } +} + +void PlatformViewIOS::AccessibilityBridgePtr::reset(AccessibilityBridge* bridge) { + if (accessibility_bridge_) { + set_semantics_enabled_(false); + } + accessibility_bridge_.reset(bridge); + if (accessibility_bridge_) { + set_semantics_enabled_(true); + } +} + PlatformViewIOS::PlatformViewIOS(PlatformView::Delegate& delegate, IOSRenderingAPI rendering_api, flutter::TaskRunners task_runners) : PlatformView(delegate, std::move(task_runners)), - ios_context_(IOSContext::Create(rendering_api)) {} + ios_context_(IOSContext::Create(rendering_api)), + accessibility_bridge_([this](bool enabled) { PlatformView::SetSemanticsEnabled(enabled); }) {} PlatformViewIOS::~PlatformViewIOS() = default; @@ -92,12 +118,13 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), void PlatformViewIOS::RegisterExternalTexture(int64_t texture_id, NSObject* texture) { - RegisterTexture(std::make_shared(texture_id, texture)); + RegisterTexture(ios_context_->CreateExternalTexture( + texture_id, fml::scoped_nsobject>{[texture retain]})); } // |PlatformView| std::unique_ptr PlatformViewIOS::CreateRenderingSurface() { - FML_DCHECK(task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()); + FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); std::lock_guard guard(ios_surface_mutex_); if (!ios_surface_) { FML_DLOG(INFO) << "Could not CreateRenderingSurface, this PlatformViewIOS " @@ -120,13 +147,14 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), return; } if (enabled && !accessibility_bridge_) { - accessibility_bridge_ = std::make_unique( - static_cast(owner_controller_.get().view), this, - [owner_controller_.get() platformViewsController]); + accessibility_bridge_.reset( + new AccessibilityBridge(static_cast(owner_controller_.get().view), this, + [owner_controller_.get() platformViewsController])); } else if (!enabled && accessibility_bridge_) { accessibility_bridge_.reset(); + } else { + PlatformView::SetSemanticsEnabled(enabled); } - PlatformView::SetSemanticsEnabled(enabled); } // |shell:PlatformView| @@ -157,7 +185,7 @@ new AccessibilityBridge(static_cast(owner_controller_.get().view), if (!owner_controller_) { return; } - [owner_controller_.get() platformViewsController] -> Reset(); + [owner_controller_.get() platformViewsController]->Reset(); } fml::scoped_nsprotocol PlatformViewIOS::GetTextInputPlugin() const { diff --git a/shell/platform/darwin/ios/rendering_api_selection.mm b/shell/platform/darwin/ios/rendering_api_selection.mm index c6ef89c47cc24..fd3ef0c297366 100644 --- a/shell/platform/darwin/ios/rendering_api_selection.mm +++ b/shell/platform/darwin/ios/rendering_api_selection.mm @@ -7,43 +7,27 @@ #include #include #include +#if FLUTTER_SHELL_ENABLE_METAL +#include +#endif // FLUTTER_SHELL_ENABLE_METAL #include "flutter/fml/logging.h" namespace flutter { -bool ShouldUseSoftwareRenderer() { - return [[[NSProcessInfo processInfo] arguments] containsObject:@"--force-software"]; -} - +#if FLUTTER_SHELL_ENABLE_METAL bool ShouldUseMetalRenderer() { - // If there is a command line argument that says Metal should not be used, that takes precedence - // over everything else. This allows disabling Metal on a per run basis to check for regressions - // on an application that has otherwise opted into Metal on an iOS version that supports it. - if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--disable-metal"]) { - return false; - } - - // If the application wants to use metal on a per run basis with disregard for version checks or - // plist based opt ins, respect that opinion. This allows selectively testing features on older - // version of iOS than those explicitly stated as being supported. - if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--force-metal"]) { - return true; - } - - // This is just a version we picked that is easy to support and has all necessary Metal features. + // Flutter supports Metal on all devices with Apple A7 SoC or above that have been updated to or + // past iOS 10.0. The processor was selected as it is the first version at which Metal was + // supported. The iOS version floor was selected due to the availability of features used by Skia. bool ios_version_supports_metal = false; - // TODO(52356): Update this to be the version selected for release. - if (@available(iOS 11.0, *)) { - ios_version_supports_metal = true; + if (@available(iOS 10.0, *)) { + auto device = MTLCreateSystemDefaultDevice(); + ios_version_supports_metal = [device supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily1_v3]; } - - // The application must opt-in by default to use Metal without command line flags. - bool application_opts_into_metal = - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"io.flutter.metal_preview"] boolValue]; - - return ios_version_supports_metal && application_opts_into_metal; + return ios_version_supports_metal; } +#endif // FLUTTER_SHELL_ENABLE_METAL IOSRenderingAPI GetRenderingAPIForProcess() { #if TARGET_IPHONE_SIMULATOR @@ -51,10 +35,6 @@ IOSRenderingAPI GetRenderingAPIForProcess() { #endif // TARGET_IPHONE_SIMULATOR #if FLUTTER_SHELL_ENABLE_METAL - static bool should_use_software = ShouldUseSoftwareRenderer(); - if (should_use_software) { - return IOSRenderingAPI::kSoftware; - } static bool should_use_metal = ShouldUseMetalRenderer(); if (should_use_metal) { return IOSRenderingAPI::kMetal; diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h b/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h index 0e3788a33d0dc..fe35a52722fe8 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h @@ -36,7 +36,7 @@ FLUTTER_EXPORT * guarantee, and are subject to change without notice. * * Note: This property WILL BE REMOVED in the future. If you use this property, please see - * https://github.com/flutter/flutter/issue/38569. + * https://github.com/flutter/flutter/issues/38569. */ @property(nullable) NSArray* engineSwitches; diff --git a/shell/platform/embedder/embedder_engine.cc b/shell/platform/embedder/embedder_engine.cc index 23202052f2096..7d6f2c5d788f1 100644 --- a/shell/platform/embedder/embedder_engine.cc +++ b/shell/platform/embedder/embedder_engine.cc @@ -232,7 +232,7 @@ bool EmbedderEngine::PostRenderThreadTask(const fml::closure& task) { return false; } - shell_->GetTaskRunners().GetGPUTaskRunner()->PostTask(task); + shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask(task); return true; } @@ -260,7 +260,8 @@ bool EmbedderEngine::PostTaskOnEngineManagedNativeThreads( // Post the task to all thread host threads. const auto& task_runners = shell_->GetTaskRunners(); - trampoline(kFlutterNativeThreadTypeRender, task_runners.GetGPUTaskRunner()); + trampoline(kFlutterNativeThreadTypeRender, + task_runners.GetRasterTaskRunner()); trampoline(kFlutterNativeThreadTypeWorker, task_runners.GetIOTaskRunner()); trampoline(kFlutterNativeThreadTypeUI, task_runners.GetUITaskRunner()); trampoline(kFlutterNativeThreadTypePlatform, diff --git a/shell/platform/embedder/embedder_external_view_embedder.cc b/shell/platform/embedder/embedder_external_view_embedder.cc index 5e77073e7ff47..23658888f7caa 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.cc +++ b/shell/platform/embedder/embedder_external_view_embedder.cc @@ -129,7 +129,8 @@ static FlutterBackingStoreConfig MakeBackingStoreConfig( } // |ExternalViewEmbedder| -bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { +bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context, + SkCanvas* background_canvas) { auto [matched_render_targets, pending_keys] = render_target_cache_.GetExistingTargetsInCache(pending_views_); @@ -265,4 +266,7 @@ bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { return true; } +// |ExternalViewEmbedder| +void EmbedderExternalViewEmbedder::FinishFrame() {} + } // namespace flutter diff --git a/shell/platform/embedder/embedder_external_view_embedder.h b/shell/platform/embedder/embedder_external_view_embedder.h index 7000d2cde04cd..63c944a88d7ef 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.h +++ b/shell/platform/embedder/embedder_external_view_embedder.h @@ -89,7 +89,10 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { SkCanvas* CompositeEmbeddedView(int view_id) override; // |ExternalViewEmbedder| - bool SubmitFrame(GrContext* context) override; + bool SubmitFrame(GrContext* context, SkCanvas* background_canvas) override; + + // |ExternalViewEmbedder| + void FinishFrame() override; // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override; diff --git a/shell/platform/embedder/embedder_thread_host.cc b/shell/platform/embedder/embedder_thread_host.cc index d5a1dc58c92ef..83860eb635d56 100644 --- a/shell/platform/embedder/embedder_thread_host.cc +++ b/shell/platform/embedder/embedder_thread_host.cc @@ -168,12 +168,12 @@ EmbedderThreadHost::CreateEmbedderManagedThreadHost( auto render_task_runner = render_task_runner_pair.second ? static_cast>( render_task_runner_pair.second) - : thread_host.gpu_thread->GetTaskRunner(); + : thread_host.raster_thread->GetTaskRunner(); flutter::TaskRunners task_runners( kFlutterThreadName, platform_task_runner, // platform - render_task_runner, // gpu + render_task_runner, // raster thread_host.ui_thread->GetTaskRunner(), // ui (always engine managed) thread_host.io_thread->GetTaskRunner() // io (always engine managed) ); @@ -219,10 +219,10 @@ EmbedderThreadHost::CreateEngineManagedThreadHost() { flutter::TaskRunners task_runners( kFlutterThreadName, - platform_task_runner, // platform - thread_host.gpu_thread->GetTaskRunner(), // gpu - thread_host.ui_thread->GetTaskRunner(), // ui - thread_host.io_thread->GetTaskRunner() // io + platform_task_runner, // platform + thread_host.raster_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io ); if (!task_runners.IsValid()) { diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 8e2cd17e05170..78286fcf54152 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -754,7 +754,7 @@ TEST_F(EmbedderTest, RasterCacheDisabledWithPlatformViews) { setup.Wait(); const flutter::Shell& shell = ToEmbedderEngine(engine.get())->GetShell(); - shell.GetTaskRunners().GetGPUTaskRunner()->PostTask([&] { + shell.GetTaskRunners().GetRasterTaskRunner()->PostTask([&] { const flutter::RasterCache& raster_cache = shell.GetRasterizer()->compositor_context()->raster_cache(); // 3 layers total, but one of them had the platform view. So the cache @@ -826,7 +826,7 @@ TEST_F(EmbedderTest, RasterCacheEnabled) { setup.Wait(); const flutter::Shell& shell = ToEmbedderEngine(engine.get())->GetShell(); - shell.GetTaskRunners().GetGPUTaskRunner()->PostTask([&] { + shell.GetTaskRunners().GetRasterTaskRunner()->PostTask([&] { const flutter::RasterCache& raster_cache = shell.GetRasterizer()->compositor_context()->raster_cache(); ASSERT_EQ(raster_cache.GetCachedEntriesCount(), 1u); @@ -1430,7 +1430,7 @@ TEST_F(EmbedderTest, }); context.GetCompositor().SetPlatformViewRendererCallback( - [&](const FlutterLayer& layer, GrContext * + [&](const FlutterLayer& layer, GrContext* /* don't use because software compositor */) -> sk_sp { auto surface = CreateRenderSurface( layer, nullptr /* null because software compositor */); @@ -1494,8 +1494,8 @@ TEST_F(EmbedderTest, } //------------------------------------------------------------------------------ -/// Custom compositor must play nicely with a custom task runner. The GPU thread -/// merging mechanism must not interfere with the custom compositor. +/// Custom compositor must play nicely with a custom task runner. The raster +/// thread merging mechanism must not interfere with the custom compositor. /// TEST_F(EmbedderTest, CustomCompositorMustWorkWithCustomTaskRunner) { auto& context = GetEmbedderContext(); @@ -3001,6 +3001,12 @@ TEST_F(EmbedderTest, VerifyB143464703WithSoftwareBackend) { auto renderered_scene = context.GetNextSceneImage(); latch.Wait(); + + // TODO(https://github.com/flutter/flutter/issues/53784): enable this on all + // platforms. +#if !defined(OS_LINUX) + GTEST_SKIP() << "Skipping golden tests on non-Linux OSes"; +#endif // OS_LINUX ASSERT_TRUE(ImageMatchesFixture("verifyb143464703_soft_noxform.png", renderered_scene)); } diff --git a/shell/platform/fuchsia/BUILD.gn b/shell/platform/fuchsia/BUILD.gn index 9afc96bc02d40..050719ec7f623 100644 --- a/shell/platform/fuchsia/BUILD.gn +++ b/shell/platform/fuchsia/BUILD.gn @@ -7,7 +7,7 @@ import("//flutter/common/config.gni") import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_host_bundle.gni") -if (using_fuchsia_sdk) { +if (is_fuchsia) { product_suffix = "" is_product = false diff --git a/shell/platform/fuchsia/dart_runner/dart_component_controller.cc b/shell/platform/fuchsia/dart_runner/dart_component_controller.cc index 02f412353e706..b558c9c171a0d 100644 --- a/shell/platform/fuchsia/dart_runner/dart_component_controller.cc +++ b/shell/platform/fuchsia/dart_runner/dart_component_controller.cc @@ -201,12 +201,12 @@ bool DartComponentController::SetupFromKernel() { } if (!dart_utils::MappedResource::LoadFromNamespace( - nullptr, "pkg/data/isolate_core_snapshot_data.bin", + nullptr, "/pkg/data/isolate_core_snapshot_data.bin", isolate_snapshot_data_)) { return false; } if (!dart_utils::MappedResource::LoadFromNamespace( - nullptr, "pkg/data/isolate_core_snapshot_instructions.bin", + nullptr, "/pkg/data/isolate_core_snapshot_instructions.bin", isolate_snapshot_instructions_, true /* executable */)) { return false; } diff --git a/shell/platform/fuchsia/dart_runner/dart_runner.cc b/shell/platform/fuchsia/dart_runner/dart_runner.cc index 74eb6b42401e8..7f72a824f1459 100644 --- a/shell/platform/fuchsia/dart_runner/dart_runner.cc +++ b/shell/platform/fuchsia/dart_runner/dart_runner.cc @@ -158,11 +158,11 @@ DartRunner::DartRunner() : context_(sys::ComponentContext::Create()) { params.vm_snapshot_instructions = ::_kDartVmSnapshotInstructions; #else if (!dart_utils::MappedResource::LoadFromNamespace( - nullptr, "pkg/data/vm_snapshot_data.bin", vm_snapshot_data_)) { + nullptr, "/pkg/data/vm_snapshot_data.bin", vm_snapshot_data_)) { FX_LOG(FATAL, LOG_TAG, "Failed to load vm snapshot data"); } if (!dart_utils::MappedResource::LoadFromNamespace( - nullptr, "pkg/data/vm_snapshot_instructions.bin", + nullptr, "/pkg/data/vm_snapshot_instructions.bin", vm_snapshot_instructions_, true /* executable */)) { FX_LOG(FATAL, LOG_TAG, "Failed to load vm snapshot instructions"); } diff --git a/shell/platform/fuchsia/dart_runner/examples/goodbye_dart/meta/goodbye_dart_aot.cmx b/shell/platform/fuchsia/dart_runner/examples/goodbye_dart/meta/goodbye_dart_aot.cmx index c6705a70ac564..e041f24109f72 100644 --- a/shell/platform/fuchsia/dart_runner/examples/goodbye_dart/meta/goodbye_dart_aot.cmx +++ b/shell/platform/fuchsia/dart_runner/examples/goodbye_dart/meta/goodbye_dart_aot.cmx @@ -3,9 +3,6 @@ "data": "data/goodbye_dart_aot" }, "sandbox": { - "features": [ - "deprecated-ambient-replace-as-executable" - ], "services": [ "fuchsia.intl.PropertyProvider", "fuchsia.sys.Environment" diff --git a/shell/platform/fuchsia/dart_runner/main.cc b/shell/platform/fuchsia/dart_runner/main.cc index 357ac33324c5d..a6fb17904bc25 100644 --- a/shell/platform/fuchsia/dart_runner/main.cc +++ b/shell/platform/fuchsia/dart_runner/main.cc @@ -31,12 +31,10 @@ static void RegisterProfilerSymbols(const char* symbols_path, #endif // !defined(DART_PRODUCT) int main(int argc, const char** argv) { - fx_log_init(); async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); std::unique_ptr provider; { - TRACE_DURATION("dart", "CreateTraceProvider"); bool already_started; // Use CreateSynchronously to prevent loss of early events. trace::TraceProviderWithFdio::CreateSynchronously( diff --git a/shell/platform/fuchsia/dart_runner/meta/dart_aot_product_runner.cmx b/shell/platform/fuchsia/dart_runner/meta/dart_aot_product_runner.cmx index 629c352e01ae3..26c6db097e343 100644 --- a/shell/platform/fuchsia/dart_runner/meta/dart_aot_product_runner.cmx +++ b/shell/platform/fuchsia/dart_runner/meta/dart_aot_product_runner.cmx @@ -5,7 +5,6 @@ "sandbox": { "features": [ "config-data", - "deprecated-ambient-replace-as-executable", "root-ssl-certificates" ], "services": [ diff --git a/shell/platform/fuchsia/dart_runner/meta/dart_aot_runner.cmx b/shell/platform/fuchsia/dart_runner/meta/dart_aot_runner.cmx index 629c352e01ae3..26c6db097e343 100644 --- a/shell/platform/fuchsia/dart_runner/meta/dart_aot_runner.cmx +++ b/shell/platform/fuchsia/dart_runner/meta/dart_aot_runner.cmx @@ -5,7 +5,6 @@ "sandbox": { "features": [ "config-data", - "deprecated-ambient-replace-as-executable", "root-ssl-certificates" ], "services": [ diff --git a/shell/platform/fuchsia/dart_runner/service_isolate.cc b/shell/platform/fuchsia/dart_runner/service_isolate.cc index 6da8a3de47e43..4906a323bbd2d 100644 --- a/shell/platform/fuchsia/dart_runner/service_isolate.cc +++ b/shell/platform/fuchsia/dart_runner/service_isolate.cc @@ -82,7 +82,7 @@ Dart_Isolate CreateServiceIsolate(const char* uri, #if defined(AOT_RUNTIME) // The VM service was compiled as a separate app. - const char* snapshot_path = "pkg/data/vmservice_snapshot.so"; + const char* snapshot_path = "/pkg/data/vmservice_snapshot.so"; if (elf_snapshot.Load(nullptr, snapshot_path)) { vmservice_data = elf_snapshot.IsolateData(); vmservice_instructions = elf_snapshot.IsolateInstrs(); @@ -92,14 +92,14 @@ Dart_Isolate CreateServiceIsolate(const char* uri, } else { // The VM service was compiled as a separate app. const char* snapshot_data_path = - "pkg/data/vmservice_isolate_snapshot_data.bin"; + "/pkg/data/vmservice_isolate_snapshot_data.bin"; const char* snapshot_instructions_path = - "pkg/data/vmservice_isolate_snapshot_instructions.bin"; + "/pkg/data/vmservice_isolate_snapshot_instructions.bin"; #else // The VM service is embedded in the core snapshot. - const char* snapshot_data_path = "pkg/data/isolate_core_snapshot_data.bin"; + const char* snapshot_data_path = "/pkg/data/isolate_core_snapshot_data.bin"; const char* snapshot_instructions_path = - "pkg/data/isolate_core_snapshot_instructions.bin"; + "/pkg/data/isolate_core_snapshot_instructions.bin"; #endif if (!dart_utils::MappedResource::LoadFromNamespace( @@ -196,7 +196,7 @@ Dart_Isolate CreateServiceIsolate(const char* uri, Dart_Handle GetVMServiceAssetsArchiveCallback() { dart_utils::MappedResource observatory_tar; if (!dart_utils::MappedResource::LoadFromNamespace( - nullptr, "pkg/data/observatory.tar", observatory_tar)) { + nullptr, "/pkg/data/observatory.tar", observatory_tar)) { FX_LOG(ERROR, LOG_TAG, "Failed to load Observatory assets"); return nullptr; } diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index 8e09a13f27d6c..bd4da2779d507 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -25,17 +25,6 @@ shell_gpu_configuration("fuchsia_gpu_configuration") { # 1. Kernel snapshot framework mode. # 2. Profiler symbols. -# Dependencies for flutter tooling -# -# While not required to run a flutter mod, these allow interacting -# with flutter via the fx tool and need to be built. -# -# This is only for builds in topaz tree. -flutter_tool_deps = [] -if (!using_fuchsia_sdk) { - flutter_tool_deps += [ "//third_party/dart-pkg/git/flutter/packages/flutter_tools:fuchsia_attach($host_toolchain)" ] -} - flutter_runner("jit") { output_name = "flutter_jit_runner" product = false @@ -50,9 +39,9 @@ flutter_runner("jit") { } extra_deps = [ - "//third_party/dart/runtime:libdart_jit", - "//third_party/dart/runtime/platform:libdart_platform_jit", - ] + flutter_tool_deps + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", + ] } flutter_runner("jit_product") { diff --git a/shell/platform/fuchsia/flutter/component.cc b/shell/platform/fuchsia/flutter/component.cc index 9f6bc6f91596c..e0d58f3e80330 100644 --- a/shell/platform/fuchsia/flutter/component.cc +++ b/shell/platform/fuchsia/flutter/component.cc @@ -431,7 +431,8 @@ class FileInNamespaceBuffer final : public fml::Mapping { FileInNamespaceBuffer(int namespace_fd, const char* path, bool executable) : address_(nullptr), size_(0) { fuchsia::mem::Buffer buffer; - if (!dart_utils::VmoFromFilenameAt(namespace_fd, path, &buffer)) { + if (!dart_utils::VmoFromFilenameAt(namespace_fd, path, executable, + &buffer)) { return; } if (buffer.size == 0) { @@ -441,17 +442,6 @@ class FileInNamespaceBuffer final : public fml::Mapping { uint32_t flags = ZX_VM_PERM_READ; if (executable) { flags |= ZX_VM_PERM_EXECUTE; - - // VmoFromFilenameAt will return VMOs without ZX_RIGHT_EXECUTE, - // so we need replace_as_executable to be able to map them as - // ZX_VM_PERM_EXECUTE. - // TODO(mdempsky): Update comment once SEC-42 is fixed. - zx_status_t status = - buffer.vmo.replace_as_executable(zx::handle(), &buffer.vmo); - if (status != ZX_OK) { - FML_LOG(FATAL) << "Failed to make VMO executable: " - << zx_status_get_string(status); - } } uintptr_t addr; zx_status_t status = diff --git a/shell/platform/fuchsia/flutter/compositor_context.cc b/shell/platform/fuchsia/flutter/compositor_context.cc index 556e80e73a66b..91513883ffcf4 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.cc +++ b/shell/platform/fuchsia/flutter/compositor_context.cc @@ -103,7 +103,7 @@ CompositorContext::AcquireFrame( const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger) { + fml::RefPtr raster_thread_merger) { // TODO: The AcquireFrame interface is too broad and must be refactored to get // rid of the context and canvas arguments as those seem to be only used for // colorspace correctness purposes on the mobile shells. diff --git a/shell/platform/fuchsia/flutter/compositor_context.h b/shell/platform/fuchsia/flutter/compositor_context.h index 46088f8e6fe36..ced422692ecfd 100644 --- a/shell/platform/fuchsia/flutter/compositor_context.h +++ b/shell/platform/fuchsia/flutter/compositor_context.h @@ -49,7 +49,7 @@ class CompositorContext final : public flutter::CompositorContext { const SkMatrix& root_surface_transformation, bool instrumentation_enabled, bool surface_supports_readback, - fml::RefPtr gpu_thread_merger) override; + fml::RefPtr raster_thread_merger) override; FML_DISALLOW_COPY_AND_ASSIGN(CompositorContext); }; diff --git a/shell/platform/fuchsia/flutter/engine.cc b/shell/platform/fuchsia/flutter/engine.cc index eb850aa52aa3d..66f4c403037d7 100644 --- a/shell/platform/fuchsia/flutter/engine.cc +++ b/shell/platform/fuchsia/flutter/engine.cc @@ -38,7 +38,7 @@ static void UpdateNativeThreadLabelNames(const std::string& label, }; set_thread_name(runners.GetPlatformTaskRunner(), label, ".platform"); set_thread_name(runners.GetUITaskRunner(), label, ".ui"); - set_thread_name(runners.GetGPUTaskRunner(), label, ".gpu"); + set_thread_name(runners.GetRasterTaskRunner(), label, ".raster"); set_thread_name(runners.GetIOTaskRunner(), label, ".io"); } @@ -154,7 +154,7 @@ Engine::Engine(Delegate& delegate, ); }); - // Session can be terminated on the GPU thread, but we must terminate + // Session can be terminated on the raster thread, but we must terminate // ourselves on the platform thread. // // This handles the fidl error callback when the Session connection is @@ -175,7 +175,7 @@ Engine::Engine(Delegate& delegate, const flutter::TaskRunners task_runners( thread_label_, // Dart thread labels CreateFMLTaskRunner(async_get_default_dispatcher()), // platform - CreateFMLTaskRunner(threads_[0]->dispatcher()), // gpu + CreateFMLTaskRunner(threads_[0]->dispatcher()), // raster CreateFMLTaskRunner(threads_[1]->dispatcher()), // ui CreateFMLTaskRunner(threads_[2]->dispatcher()) // io ); @@ -474,7 +474,7 @@ void Engine::OnSessionMetricsDidChange( return; } - shell_->GetTaskRunners().GetGPUTaskRunner()->PostTask( + shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( [rasterizer = shell_->GetRasterizer(), metrics]() { if (rasterizer) { auto compositor_context = @@ -491,7 +491,7 @@ void Engine::OnDebugWireframeSettingsChanged(bool enabled) { return; } - shell_->GetTaskRunners().GetGPUTaskRunner()->PostTask( + shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( [rasterizer = shell_->GetRasterizer(), enabled]() { if (rasterizer) { auto compositor_context = @@ -509,7 +509,7 @@ void Engine::OnSessionSizeChangeHint(float width_change_factor, return; } - shell_->GetTaskRunners().GetGPUTaskRunner()->PostTask( + shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask( [rasterizer = shell_->GetRasterizer(), width_change_factor, height_change_factor]() { if (rasterizer) { diff --git a/shell/platform/fuchsia/flutter/main.cc b/shell/platform/fuchsia/flutter/main.cc index e37a12d275634..8fa2dfe803a7b 100644 --- a/shell/platform/fuchsia/flutter/main.cc +++ b/shell/platform/fuchsia/flutter/main.cc @@ -17,7 +17,6 @@ int main(int argc, char const* argv[]) { std::unique_ptr provider; { - TRACE_DURATION("flutter", "CreateTraceProvider"); bool already_started; // Use CreateSynchronously to prevent loss of early events. trace::TraceProviderWithFdio::CreateSynchronously( diff --git a/shell/platform/fuchsia/flutter/meta/flutter_aot_product_runner.cmx b/shell/platform/fuchsia/flutter/meta/flutter_aot_product_runner.cmx index 019dc11340d84..914c22b6f2dbb 100644 --- a/shell/platform/fuchsia/flutter/meta/flutter_aot_product_runner.cmx +++ b/shell/platform/fuchsia/flutter/meta/flutter_aot_product_runner.cmx @@ -5,7 +5,6 @@ "sandbox": { "features": [ "config-data", - "deprecated-ambient-replace-as-executable", "root-ssl-certificates", "vulkan" ], diff --git a/shell/platform/fuchsia/flutter/meta/flutter_aot_runner.cmx b/shell/platform/fuchsia/flutter/meta/flutter_aot_runner.cmx index af0c724590652..11ba40d7870f2 100644 --- a/shell/platform/fuchsia/flutter/meta/flutter_aot_runner.cmx +++ b/shell/platform/fuchsia/flutter/meta/flutter_aot_runner.cmx @@ -5,7 +5,6 @@ "sandbox": { "features": [ "config-data", - "deprecated-ambient-replace-as-executable", "root-ssl-certificates", "vulkan" ], diff --git a/shell/platform/fuchsia/flutter/runner.cc b/shell/platform/fuchsia/flutter/runner.cc index 2fe0b5626ada6..8933f963df36b 100644 --- a/shell/platform/fuchsia/flutter/runner.cc +++ b/shell/platform/fuchsia/flutter/runner.cc @@ -75,7 +75,7 @@ bool InitializeTZData() { << strerror(errno); return false; } - if (!close(fd)) { + if (close(fd)) { FML_LOG(WARNING) << "Could not close: " << tzdata_dir << ": " << strerror(errno); } @@ -87,7 +87,7 @@ bool InitializeICU() { const char* data_path = kIcuDataPath; fuchsia::mem::Buffer icu_data; - if (!dart_utils::VmoFromFilename(data_path, &icu_data)) { + if (!dart_utils::VmoFromFilename(data_path, false, &icu_data)) { return false; } diff --git a/shell/platform/fuchsia/flutter/session_connection.h b/shell/platform/fuchsia/flutter/session_connection.h index e0855753c6b17..54ba846ee8974 100644 --- a/shell/platform/fuchsia/flutter/session_connection.h +++ b/shell/platform/fuchsia/flutter/session_connection.h @@ -27,7 +27,7 @@ namespace flutter_runner { using on_frame_presented_event = std::function; -// The component residing on the GPU thread that is responsible for +// The component residing on the raster thread that is responsible for // maintaining the Scenic session connection and presenting node updates. class SessionConnection final { public: diff --git a/shell/platform/fuchsia/flutter/surface.h b/shell/platform/fuchsia/flutter/surface.h index 66220f079b3e5..c1ca54be1baad 100644 --- a/shell/platform/fuchsia/flutter/surface.h +++ b/shell/platform/fuchsia/flutter/surface.h @@ -12,8 +12,8 @@ namespace flutter_runner { // The interface between the Flutter rasterizer and the underlying platform. May -// be constructed on any thread but will be used by the engine only on the GPU -// thread. +// be constructed on any thread but will be used by the engine only on the +// raster thread. class Surface final : public flutter::Surface { public: Surface(std::string debug_label); diff --git a/shell/platform/fuchsia/flutter/vsync_waiter.cc b/shell/platform/fuchsia/flutter/vsync_waiter.cc index 283a33e14a6ad..6d2be8d0a0384 100644 --- a/shell/platform/fuchsia/flutter/vsync_waiter.cc +++ b/shell/platform/fuchsia/flutter/vsync_waiter.cc @@ -141,7 +141,7 @@ void VsyncWaiter::AwaitVSync() { fml::TimeDelta delta = next_vsync_start_time - now; task_runners_.GetUITaskRunner()->PostDelayedTask( - [& weak_factory_ui = this->weak_factory_ui_] { + [&weak_factory_ui = this->weak_factory_ui_] { if (!weak_factory_ui) { FML_LOG(WARNING) << "WeakPtrFactory for VsyncWaiter is null, likely " "due to the VsyncWaiter being destroyed."; diff --git a/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc b/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc index f1bcfc12ba350..83e71d64c6109 100644 --- a/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc +++ b/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc @@ -62,7 +62,7 @@ TEST_F(VsyncWaiterTest, AwaitVsync) { "VsyncWaiterTests", // Dart thread labels flutter_runner::CreateFMLTaskRunner( async_get_default_dispatcher()), // platform - flutter_runner::CreateFMLTaskRunner(threads[0]->dispatcher()), // gpu + flutter_runner::CreateFMLTaskRunner(threads[0]->dispatcher()), // raster flutter_runner::CreateFMLTaskRunner(threads[1]->dispatcher()), // ui flutter_runner::CreateFMLTaskRunner(threads[2]->dispatcher()) // io ); diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.cc b/shell/platform/fuchsia/flutter/vulkan_surface.cc index abbbe5d945742..781f9ae211c24 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface.cc @@ -73,7 +73,7 @@ bool CreateVulkanImage(vulkan::VulkanProvider& vulkan_provider, } out_vulkan_image->vk_image = { - vk_image, [& vulkan_provider = vulkan_provider](VkImage image) { + vk_image, [&vulkan_provider = vulkan_provider](VkImage image) { vulkan_provider.vk().DestroyImage(vulkan_provider.vk_device(), image, NULL); }}; @@ -263,8 +263,8 @@ bool VulkanSurface::AllocateDeviceMemory(sk_sp context, return false; } - vk_memory_ = {vk_memory, [& vulkan_provider = - vulkan_provider_](VkDeviceMemory memory) { + vk_memory_ = {vk_memory, + [&vulkan_provider = vulkan_provider_](VkDeviceMemory memory) { vulkan_provider.vk().FreeMemory(vulkan_provider.vk_device(), memory, NULL); }}; diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.h b/shell/platform/fuchsia/flutter/vulkan_surface.h index 4c67c9d5538c2..2315e0bcee70e 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface.h @@ -133,10 +133,11 @@ class VulkanSurface final // For better safety in retained rendering, Flutter uses a retained // |EntityNode| associated with the retained surface instead of using the // retained surface directly. Hence Flutter can't modify the surface during - // retained rendering. - const scenic::EntityNode& GetRetainedNode() { + // retained rendering. However, the node itself is modifiable to be able + // to adjust its position. + scenic::EntityNode* GetRetainedNode() { used_in_retained_rendering_ = true; - return *retained_node_; + return retained_node_.get(); } // Check whether the retained surface (and its associated |EntityNode|) is diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h index 00b40437de612..23f8551356265 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_pool.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_pool.h @@ -43,8 +43,7 @@ class VulkanSurfacePool final { return retained_surfaces_.find(key) != retained_surfaces_.end(); } // For |VulkanSurfaceProducer::GetRetainedNode|. - const scenic::EntityNode& GetRetainedNode( - const flutter::LayerRasterCacheKey& key) { + scenic::EntityNode* GetRetainedNode(const flutter::LayerRasterCacheKey& key) { FML_DCHECK(HasRetainedNode(key)); return retained_surfaces_[key].vk_surface->GetRetainedNode(); } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h index 77457bac32cdd..5ea1415cf537f 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h @@ -49,7 +49,7 @@ class VulkanSurfaceProducer final } // |flutter::SceneUpdateContext::GetRetainedNode| - const scenic::EntityNode& GetRetainedNode( + scenic::EntityNode* GetRetainedNode( const flutter::LayerRasterCacheKey& key) override { return surface_pool_->GetRetainedNode(key); } diff --git a/shell/platform/fuchsia/runtime/dart/utils/handle_exception.cc b/shell/platform/fuchsia/runtime/dart/utils/handle_exception.cc index 6785a1220ed68..3ef03565d904e 100644 --- a/shell/platform/fuchsia/runtime/dart/utils/handle_exception.cc +++ b/shell/platform/fuchsia/runtime/dart/utils/handle_exception.cc @@ -64,6 +64,11 @@ fuchsia::feedback::CrashReport BuildCrashReport( error.substr(delimiter_pos + 2 /*to get rid of the leading ': '*/); } + // Truncate error message to the maximum length allowed for the crash_reporter + // FIDL call + error_message = error_message.substr( + 0, fuchsia::feedback::MAX_EXCEPTION_MESSAGE_LENGTH - 1); + fuchsia::feedback::RuntimeCrashReport dart_report; dart_report.set_exception_type(error_type); dart_report.set_exception_message(error_message); diff --git a/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc b/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc index d8837e6fbc27f..0224cf25f714d 100644 --- a/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc +++ b/shell/platform/fuchsia/runtime/dart/utils/mapped_resource.cc @@ -32,41 +32,31 @@ static bool OpenVmo(fuchsia::mem::Buffer* resource_vmo, bool executable) { TRACE_DURATION("dart", "LoadFromNamespace", "path", path); - // openat of a path with a leading '/' ignores the namespace fd. - dart_utils::Check(path[0] != '/', LOG_TAG); - if (namespc == nullptr) { - if (!VmoFromFilename(path, resource_vmo)) { + // Opening a file in the root namespace expects an absolute path. + dart_utils::Check(path[0] == '/', LOG_TAG); + if (!VmoFromFilename(path, executable, resource_vmo)) { return false; } } else { + // openat of a path with a leading '/' ignores the namespace fd. + // require a relative path. + dart_utils::Check(path[0] != '/', LOG_TAG); + auto root_dir = fdio_ns_opendir(namespc); if (root_dir < 0) { FX_LOG(ERROR, LOG_TAG, "Failed to open namespace directory"); return false; } - bool result = dart_utils::VmoFromFilenameAt(root_dir, path, resource_vmo); + bool result = + dart_utils::VmoFromFilenameAt(root_dir, path, executable, resource_vmo); close(root_dir); if (!result) { return result; } } - if (executable) { - // VmoFromFilenameAt will return VMOs without ZX_RIGHT_EXECUTE, - // so we need replace_as_executable to be able to map them as - // ZX_VM_PERM_EXECUTE. - // TODO(mdempsky): Update comment once SEC-42 is fixed. - zx_status_t status = resource_vmo->vmo.replace_as_executable( - zx::handle(), &resource_vmo->vmo); - if (status != ZX_OK) { - FX_LOGF(ERROR, LOG_TAG, "Failed to make VMO executable: %s", - zx_status_get_string(status)); - return false; - } - } - return true; } @@ -114,10 +104,22 @@ MappedResource::~MappedResource() { static int OpenFdExec(const std::string& path, int dirfd) { int fd = -1; - zx_status_t result = fdio_open_fd_at( - dirfd, path.c_str(), - fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_EXECUTABLE, - &fd); + zx_status_t result; + if (dirfd == AT_FDCWD) { + // fdio_open_fd_at does not support AT_FDCWD, by design. Use fdio_open_fd + // and expect an absolute path for that usage pattern. + dart_utils::Check(path[0] == '/', LOG_TAG); + result = fdio_open_fd( + path.c_str(), + fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_EXECUTABLE, + &fd); + } else { + dart_utils::Check(path[0] != '/', LOG_TAG); + result = fdio_open_fd_at( + dirfd, path.c_str(), + fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_EXECUTABLE, + &fd); + } if (result != ZX_OK) { FX_LOGF(ERROR, LOG_TAG, "fdio_open_fd_at(%s) failed: %s", path.c_str(), zx_status_get_string(result)); diff --git a/shell/platform/fuchsia/runtime/dart/utils/vmo.cc b/shell/platform/fuchsia/runtime/dart/utils/vmo.cc index 130181c777bce..87f181e38b49c 100644 --- a/shell/platform/fuchsia/runtime/dart/utils/vmo.cc +++ b/shell/platform/fuchsia/runtime/dart/utils/vmo.cc @@ -7,15 +7,18 @@ #include #include +#include #include +#include #include #include +#include #include "runtime/dart/utils/logging.h" namespace { -bool VmoFromFd(int fd, fuchsia::mem::Buffer* buffer) { +bool VmoFromFd(int fd, bool executable, fuchsia::mem::Buffer* buffer) { if (!buffer) { FX_LOG(FATAL, LOG_TAG, "Invalid buffer pointer"); } @@ -27,7 +30,14 @@ bool VmoFromFd(int fd, fuchsia::mem::Buffer* buffer) { } zx_handle_t result = ZX_HANDLE_INVALID; - if (fdio_get_vmo_copy(fd, &result) != ZX_OK) { + zx_status_t status; + if (executable) { + status = fdio_get_vmo_exec(fd, &result); + } else { + status = fdio_get_vmo_copy(fd, &result); + } + + if (status != ZX_OK) { return false; } @@ -42,20 +52,42 @@ bool VmoFromFd(int fd, fuchsia::mem::Buffer* buffer) { namespace dart_utils { bool VmoFromFilename(const std::string& filename, + bool executable, fuchsia::mem::Buffer* buffer) { - return VmoFromFilenameAt(AT_FDCWD, filename, buffer); + // Note: the implementation here cannot be shared with VmoFromFilenameAt + // because fdio_open_fd_at does not aim to provide POSIX compatibility, and + // thus does not handle AT_FDCWD as dirfd. + uint32_t flags = fuchsia::io::OPEN_RIGHT_READABLE | + (executable ? fuchsia::io::OPEN_RIGHT_EXECUTABLE : 0); + zx_status_t status; + int fd; + + status = fdio_open_fd(filename.c_str(), flags, &fd); + if (status != ZX_OK) { + FX_LOGF(ERROR, LOG_TAG, "fdio_open_fd(\"%s\", %08x) failed: %s", + filename.c_str(), flags, zx_status_get_string(status)); + return false; + } + bool result = VmoFromFd(fd, executable, buffer); + close(fd); + return result; } bool VmoFromFilenameAt(int dirfd, const std::string& filename, + bool executable, fuchsia::mem::Buffer* buffer) { - int fd = openat(dirfd, filename.c_str(), O_RDONLY); - if (fd == -1) { - FX_LOGF(ERROR, LOG_TAG, "openat(\"%s\") failed: %s", filename.c_str(), - strerror(errno)); + uint32_t flags = fuchsia::io::OPEN_RIGHT_READABLE | + (executable ? fuchsia::io::OPEN_RIGHT_EXECUTABLE : 0); + zx_status_t status; + int fd; + status = fdio_open_fd_at(dirfd, filename.c_str(), flags, &fd); + if (status != ZX_OK) { + FX_LOGF(ERROR, LOG_TAG, "fdio_open_fd_at(%d, \"%s\", %08x) failed: %s", + dirfd, filename.c_str(), flags, zx_status_get_string(status)); return false; } - bool result = VmoFromFd(fd, buffer); + bool result = VmoFromFd(fd, executable, buffer); close(fd); return result; } diff --git a/shell/platform/fuchsia/runtime/dart/utils/vmo.h b/shell/platform/fuchsia/runtime/dart/utils/vmo.h index bc4065eb815e4..52871d2e32751 100644 --- a/shell/platform/fuchsia/runtime/dart/utils/vmo.h +++ b/shell/platform/fuchsia/runtime/dart/utils/vmo.h @@ -11,10 +11,13 @@ namespace dart_utils { -bool VmoFromFilename(const std::string& filename, fuchsia::mem::Buffer* buffer); +bool VmoFromFilename(const std::string& filename, + bool executable, + fuchsia::mem::Buffer* buffer); bool VmoFromFilenameAt(int dirfd, const std::string& filename, + bool executable, fuchsia::mem::Buffer* buffer); zx_status_t IsSizeValid(const fuchsia::mem::Buffer& buffer, bool* is_valid); diff --git a/shell/platform/glfw/BUILD.gn b/shell/platform/glfw/BUILD.gn index 546718bc14158..c78eef6183690 100644 --- a/shell/platform/glfw/BUILD.gn +++ b/shell/platform/glfw/BUILD.gn @@ -42,8 +42,6 @@ source_set("flutter_glfw") { "text_input_plugin.h", ] - defines = [ "USE_RAPID_JSON" ] - configs += [ "//flutter/shell/platform/common/cpp:desktop_library_implementation" ] diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc index da1176f4f0f5b..74a5be634694e 100644 --- a/shell/platform/glfw/flutter_glfw.cc +++ b/shell/platform/glfw/flutter_glfw.cc @@ -10,10 +10,12 @@ #include #include #include +#include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" +#include "flutter/shell/platform/common/cpp/path_utils.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/glfw/glfw_event_loop.h" #include "flutter/shell/platform/glfw/key_event_handler.h" @@ -116,6 +118,9 @@ struct FlutterDesktopPluginRegistrar { // The handle for the window associated with this registrar. FlutterDesktopWindow* window; + + // Callback to be called on registrar destruction. + FlutterDesktopOnRegistrarDestroyed destruction_handler; }; // State associated with the messenger used to communicate with the engine. @@ -507,6 +512,25 @@ static FLUTTER_API_SYMBOL(FlutterEngine) &engine_properties.switches[engine_properties.switches_count]); } + std::filesystem::path assets_path = + std::filesystem::u8path(engine_properties.assets_path); + std::filesystem::path icu_path = + std::filesystem::u8path(engine_properties.icu_data_path); + if (assets_path.is_relative() || icu_path.is_relative()) { + // Treat relative paths as relative to the directory of this executable. + std::filesystem::path executable_location = + flutter::GetExecutableDirectory(); + if (executable_location.empty()) { + std::cerr << "Unable to find executable location to resolve paths." + << std::endl; + return nullptr; + } + assets_path = std::filesystem::path(executable_location) / assets_path; + icu_path = std::filesystem::path(executable_location) / icu_path; + } + std::string assets_path_string = assets_path.u8string(); + std::string icu_path_string = icu_path.u8string(); + FlutterRendererConfig config = {}; if (window == nullptr) { config.type = kOpenGL; @@ -528,8 +552,8 @@ static FLUTTER_API_SYMBOL(FlutterEngine) } FlutterProjectArgs args = {}; args.struct_size = sizeof(FlutterProjectArgs); - args.assets_path = engine_properties.assets_path; - args.icu_data_path = engine_properties.icu_data_path; + args.assets_path = assets_path_string.c_str(); + args.icu_data_path = icu_path_string.c_str(); args.command_line_argc = static_cast(argv.size()); args.command_line_argv = &argv[0]; args.platform_message_callback = GLFWOnFlutterPlatformMessage; @@ -661,6 +685,11 @@ FlutterDesktopWindowControllerRef FlutterDesktopCreateWindow( } void FlutterDesktopDestroyWindow(FlutterDesktopWindowControllerRef controller) { + FlutterDesktopPluginRegistrarRef registrar = + controller->plugin_registrar.get(); + if (registrar->destruction_handler) { + registrar->destruction_handler(registrar); + } FlutterEngineShutdown(controller->engine); delete controller; } @@ -811,6 +840,12 @@ FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger( return registrar->messenger.get(); } +void FlutterDesktopRegistrarSetDestructionHandler( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopOnRegistrarDestroyed callback) { + registrar->destruction_handler = callback; +} + FlutterDesktopWindowRef FlutterDesktopRegistrarGetWindow( FlutterDesktopPluginRegistrarRef registrar) { return registrar->window; diff --git a/shell/platform/glfw/key_event_handler.cc b/shell/platform/glfw/key_event_handler.cc index 2ad013c064462..bdd121091f54e 100644 --- a/shell/platform/glfw/key_event_handler.cc +++ b/shell/platform/glfw/key_event_handler.cc @@ -6,7 +6,7 @@ #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h" +#include "flutter/shell/platform/common/cpp/json_message_codec.h" static constexpr char kChannelName[] = "flutter/keyevent"; diff --git a/shell/platform/glfw/platform_handler.cc b/shell/platform/glfw/platform_handler.cc index 39c8b4708cd75..6d4046364abc6 100644 --- a/shell/platform/glfw/platform_handler.cc +++ b/shell/platform/glfw/platform_handler.cc @@ -6,7 +6,7 @@ #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h" +#include "flutter/shell/platform/common/cpp/json_method_codec.h" static constexpr char kChannelName[] = "flutter/platform"; diff --git a/shell/platform/glfw/public/flutter_glfw.h b/shell/platform/glfw/public/flutter_glfw.h index 610c63ff7948a..f9921bf36ca33 100644 --- a/shell/platform/glfw/public/flutter_glfw.h +++ b/shell/platform/glfw/public/flutter_glfw.h @@ -38,8 +38,12 @@ typedef struct { // Properties for configuring a Flutter engine instance. typedef struct { // The path to the flutter_assets folder for the application to be run. + // This can either be an absolute path, or on Windows or Linux, a path + // relative to the directory containing the executable. const char* assets_path; // The path to the icudtl.dat file for the version of Flutter you are using. + // This can either be an absolute path, or on Windows or Linux, a path + // relative to the directory containing the executable. const char* icu_data_path; // The switches to pass to the Flutter engine. // diff --git a/shell/platform/glfw/text_input_plugin.cc b/shell/platform/glfw/text_input_plugin.cc index 94fa0a267cf5e..d1f163b2f1065 100644 --- a/shell/platform/glfw/text_input_plugin.cc +++ b/shell/platform/glfw/text_input_plugin.cc @@ -7,7 +7,7 @@ #include #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h" +#include "flutter/shell/platform/common/cpp/json_method_codec.h" static constexpr char kSetEditingStateMethod[] = "TextInput.setEditingState"; static constexpr char kClearClientMethod[] = "TextInput.clearClient"; diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 54e1bbbd7c709..d5ac69df74e6a 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -7,8 +7,12 @@ assert(is_linux) import("//flutter/shell/platform/glfw/config.gni") group("linux") { + deps = [ + ":flutter_linux_gtk", + ":publish_headers_linux", + ] if (build_glfw_shell) { - deps = [ + deps += [ ":flutter_linux_glfw", "//flutter/shell/platform/glfw:publish_headers_glfw", "//flutter/shell/platform/glfw/client_wrapper:publish_wrapper_glfw", @@ -37,3 +41,50 @@ if (build_glfw_shell) { public_configs = [ "//flutter:config" ] } } + +_public_headers = [ + "public/flutter_linux/fl_dart_project.h", + "public/flutter_linux/fl_view.h", + "public/flutter_linux/flutter_linux.h", +] + +config("relative_flutter_linux_headers") { + include_dirs = [ "public" ] +} + +source_set("flutter_linux") { + public = _public_headers + + sources = [ + "fl_dart_project.cc", + "fl_view.cc", + ] + + configs += [ + "//flutter/shell/platform/linux/config:gtk", + "//flutter/shell/platform/linux/config:egl", + "//third_party/khronos:khronos_headers", + ] + + # Set flag to stop headers being directly included (library users should not do this) + defines = [ "FLUTTER_LINUX_COMPILATION" ] + + deps = [ + "//flutter/shell/platform/embedder:embedder_with_symbol_prefix", + ] +} + +shared_library("flutter_linux_gtk") { + deps = [ + ":flutter_linux", + ] + + public_configs = [ "//flutter:config" ] +} + +copy("publish_headers_linux") { + sources = _public_headers + outputs = [ + "$root_out_dir/flutter_linux/{{source_file_part}}", + ] +} diff --git a/shell/platform/linux/config/BUILD.gn b/shell/platform/linux/config/BUILD.gn index 48cf022355cff..d188f2dd6389b 100644 --- a/shell/platform/linux/config/BUILD.gn +++ b/shell/platform/linux/config/BUILD.gn @@ -7,3 +7,11 @@ import("//build/config/linux/pkg_config.gni") pkg_config("x11") { packages = [ "x11" ] } + +pkg_config("gtk") { + packages = [ "gtk+-3.0" ] +} + +pkg_config("egl") { + packages = [ "egl" ] +} diff --git a/shell/platform/linux/fl_dart_project.cc b/shell/platform/linux/fl_dart_project.cc new file mode 100644 index 0000000000000..fcaeff2248618 --- /dev/null +++ b/shell/platform/linux/fl_dart_project.cc @@ -0,0 +1,126 @@ +// 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/shell/platform/linux/public/flutter_linux/fl_dart_project.h" + +#include + +struct _FlDartProject { + GObject parent_instance; + + gchar* path; +}; + +enum { PROP_ASSETS_PATH = 1, PROP_ICU_DATA_PATH, PROP_PATH, PROP_LAST }; + +G_DEFINE_TYPE(FlDartProject, fl_dart_project, G_TYPE_OBJECT) + +static void fl_dart_project_set_path(FlDartProject* self, const gchar* path) { + g_free(self->path); + + if (g_path_is_absolute(path)) + self->path = g_strdup(path); + else { + g_autoptr(GError) error = NULL; + g_autofree gchar* exe_path = g_file_read_link("/proc/self/exe", &error); + if (exe_path == NULL) + g_critical("Failed to determine location of executable: %s", + error->message); + g_autofree gchar* dir = g_path_get_dirname(exe_path); + self->path = g_build_filename(dir, path, NULL); + } +} + +static void fl_dart_project_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec) { + FlDartProject* self = FL_DART_PROJECT(object); + + switch (prop_id) { + case PROP_PATH: + fl_dart_project_set_path(self, g_value_get_string(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_dart_project_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec) { + FlDartProject* self = FL_DART_PROJECT(object); + + switch (prop_id) { + case PROP_ASSETS_PATH: + g_value_take_string(value, fl_dart_project_get_assets_path(self)); + break; + case PROP_ICU_DATA_PATH: + g_value_take_string(value, fl_dart_project_get_icu_data_path(self)); + break; + case PROP_PATH: + g_value_set_string(value, self->path); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_dart_project_dispose(GObject* object) { + FlDartProject* self = FL_DART_PROJECT(object); + + g_clear_pointer(&self->path, g_free); + + G_OBJECT_CLASS(fl_dart_project_parent_class)->dispose(object); +} + +static void fl_dart_project_class_init(FlDartProjectClass* klass) { + G_OBJECT_CLASS(klass)->set_property = fl_dart_project_set_property; + G_OBJECT_CLASS(klass)->get_property = fl_dart_project_get_property; + G_OBJECT_CLASS(klass)->dispose = fl_dart_project_dispose; + + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_ASSETS_PATH, + g_param_spec_string( + "assets-path", "assets-path", "Path to Flutter assets", nullptr, + static_cast(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_ICU_DATA_PATH, + g_param_spec_string( + "icu-data-path", "icu-data-path", "Path to ICU data", nullptr, + static_cast(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_PATH, + g_param_spec_string( + "path", "path", "Path to Flutter project", nullptr, + static_cast(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); +} + +static void fl_dart_project_init(FlDartProject* self) {} + +G_MODULE_EXPORT FlDartProject* fl_dart_project_new(const gchar* path) { + return static_cast( + g_object_new(fl_dart_project_get_type(), "path", path, nullptr)); +} + +G_MODULE_EXPORT const gchar* fl_dart_project_get_path(FlDartProject* self) { + g_return_val_if_fail(FL_IS_DART_PROJECT(self), nullptr); + return self->path; +} + +G_MODULE_EXPORT gchar* fl_dart_project_get_assets_path(FlDartProject* self) { + g_return_val_if_fail(FL_IS_DART_PROJECT(self), nullptr); + return g_build_filename(self->path, "flutter_assets", NULL); +} + +G_MODULE_EXPORT gchar* fl_dart_project_get_icu_data_path(FlDartProject* self) { + g_return_val_if_fail(FL_IS_DART_PROJECT(self), nullptr); + return g_build_filename(self->path, "icudtl.dat", NULL); +} diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc new file mode 100644 index 0000000000000..a1aad509a5230 --- /dev/null +++ b/shell/platform/linux/fl_view.cc @@ -0,0 +1,278 @@ +// 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/shell/platform/linux/public/flutter_linux/fl_view.h" + +#include +#include +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" + +struct _FlView { + GtkWidget parent_instance; + + EGLDisplay egl_display; + EGLSurface egl_surface; + EGLContext egl_context; + + FlDartProject* flutter_project; + FLUTTER_API_SYMBOL(FlutterEngine) flutter_engine; +}; + +enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST }; + +G_DEFINE_TYPE(FlView, fl_view, GTK_TYPE_WIDGET) + +static gboolean initialize_egl(FlView* self) { + /* Note that we don't provide the XDisplay from GTK, this would make both + * GTK and EGL share the same X connection and this would crash when used by + * a Flutter thread. So the EGL display and GTK both have separate + * connections. + */ + self->egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + + EGLint egl_major, egl_minor; + if (!eglInitialize(self->egl_display, &egl_major, &egl_minor)) { + g_warning("Failed to initialze EGL"); + return FALSE; + } + // TODO(robert-ancell): It would probably be useful to store the EGL version + // for debugging purposes + + EGLint attributes[] = {EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + EGL_NONE}; + EGLConfig egl_config; + EGLint n_config; + if (!eglChooseConfig(self->egl_display, attributes, &egl_config, 1, + &n_config)) { + g_warning("Failed to choose EGL config"); + return FALSE; + } + if (n_config == 0) { + g_warning("Failed to find appropriate EGL config"); + return FALSE; + } + if (!eglBindAPI(EGL_OPENGL_ES_API)) { + g_warning("Failed to bind EGL OpenGL ES API"); + return FALSE; + } + + Window xid = gdk_x11_window_get_xid(gtk_widget_get_window(GTK_WIDGET(self))); + self->egl_surface = + eglCreateWindowSurface(self->egl_display, egl_config, xid, nullptr); + EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + self->egl_context = eglCreateContext(self->egl_display, egl_config, + EGL_NO_CONTEXT, context_attributes); + EGLint value; + eglQueryContext(self->egl_display, self->egl_context, + EGL_CONTEXT_CLIENT_VERSION, &value); + + return TRUE; +} + +static void* fl_view_gl_proc_resolver(void* user_data, const char* name) { + return reinterpret_cast(eglGetProcAddress(name)); +} + +static bool fl_view_gl_make_current(void* user_data) { + FlView* self = static_cast(user_data); + + if (!eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, + self->egl_context)) + g_warning("Failed to make EGL context current"); + + return true; +} + +static bool fl_view_gl_clear_current(void* user_data) { + FlView* self = static_cast(user_data); + + if (!eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT)) + g_warning("Failed to make EGL context current"); + + return true; +} + +static uint32_t fl_view_gl_fbo_callback(void* user_data) { + /* There is only one frame buffer object - always return that */ + return 0; +} + +static bool fl_view_gl_present(void* user_data) { + FlView* self = static_cast(user_data); + + if (!eglSwapBuffers(self->egl_display, self->egl_surface)) + g_warning("Failed to swap EGL buffers"); + + return true; +} + +static gboolean run_flutter_engine(FlView* self) { + FlutterRendererConfig config = {}; + config.type = kOpenGL; + config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig); + config.open_gl.gl_proc_resolver = fl_view_gl_proc_resolver; + config.open_gl.make_current = fl_view_gl_make_current; + config.open_gl.clear_current = fl_view_gl_clear_current; + config.open_gl.fbo_callback = fl_view_gl_fbo_callback; + config.open_gl.present = fl_view_gl_present; + + g_autofree gchar* assets_path = + fl_dart_project_get_assets_path(self->flutter_project); + g_autofree gchar* icu_data_path = + fl_dart_project_get_icu_data_path(self->flutter_project); + + FlutterProjectArgs args = {}; + args.struct_size = sizeof(FlutterProjectArgs); + args.assets_path = assets_path; + args.icu_data_path = icu_data_path; + + FlutterEngineResult result = FlutterEngineInitialize( + FLUTTER_ENGINE_VERSION, &config, &args, self, &self->flutter_engine); + if (result != kSuccess) + return FALSE; + + result = FlutterEngineRunInitialized(self->flutter_engine); + if (result != kSuccess) + return FALSE; + + return TRUE; +} + +static void fl_view_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec) { + FlView* self = FL_VIEW(object); + + switch (prop_id) { + case PROP_FLUTTER_PROJECT: + g_set_object(&self->flutter_project, + static_cast(g_value_get_object(value))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_view_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec) { + FlView* self = FL_VIEW(object); + + switch (prop_id) { + case PROP_FLUTTER_PROJECT: + g_value_set_object(value, self->flutter_project); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void fl_view_dispose(GObject* object) { + FlView* self = FL_VIEW(object); + + FlutterEngineDeinitialize(self->flutter_engine); + FlutterEngineShutdown(self->flutter_engine); + + if (!eglDestroyContext(self->egl_display, self->egl_context)) + g_warning("Failed to destroy EGL context"); + if (!eglDestroySurface(self->egl_display, self->egl_surface)) + g_warning("Failed to destroy EGL surface"); + if (!eglTerminate(self->egl_display)) + g_warning("Failed to terminate EGL display"); + + g_clear_object(&self->flutter_project); + + G_OBJECT_CLASS(fl_view_parent_class)->dispose(object); +} + +static void fl_view_realize(GtkWidget* widget) { + FlView* self = FL_VIEW(widget); + + gtk_widget_set_realized(widget, TRUE); + + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + + GdkWindowAttr window_attributes; + window_attributes.window_type = GDK_WINDOW_CHILD; + window_attributes.x = allocation.x; + window_attributes.y = allocation.y; + window_attributes.width = allocation.width; + window_attributes.height = allocation.height; + window_attributes.wclass = GDK_INPUT_OUTPUT; + window_attributes.visual = gtk_widget_get_visual(widget); + window_attributes.event_mask = + gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK; + + gint window_attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + GdkWindow* window = + gdk_window_new(gtk_widget_get_parent_window(widget), &window_attributes, + window_attributes_mask); + gtk_widget_register_window(widget, window); + gtk_widget_set_window(widget, window); + + if (initialize_egl(self)) + run_flutter_engine(self); +} + +static void fl_view_size_allocate(GtkWidget* widget, + GtkAllocation* allocation) { + FlView* self = FL_VIEW(widget); + + gtk_widget_set_allocation(widget, allocation); + + if (gtk_widget_get_realized(widget) && gtk_widget_get_has_window(widget)) + gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, + allocation->y, allocation->width, + allocation->height); + + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(FlutterWindowMetricsEvent); + event.width = allocation->width; + event.height = allocation->height; + event.pixel_ratio = + 1; // TODO(robert-ancell): This won't work on hidpi displays + FlutterEngineSendWindowMetricsEvent(self->flutter_engine, &event); +} + +static void fl_view_class_init(FlViewClass* klass) { + G_OBJECT_CLASS(klass)->set_property = fl_view_set_property; + G_OBJECT_CLASS(klass)->get_property = fl_view_get_property; + G_OBJECT_CLASS(klass)->dispose = fl_view_dispose; + GTK_WIDGET_CLASS(klass)->realize = fl_view_realize; + GTK_WIDGET_CLASS(klass)->size_allocate = fl_view_size_allocate; + + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_FLUTTER_PROJECT, + g_param_spec_object( + "flutter-project", "flutter-project", "Flutter project in use", + fl_dart_project_get_type(), + static_cast(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); +} + +static void fl_view_init(FlView* self) {} + +G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) { + return static_cast( + g_object_new(fl_view_get_type(), "flutter-project", project, nullptr)); +} diff --git a/shell/platform/linux/public/flutter_linux/fl_dart_project.h b/shell/platform/linux/public/flutter_linux/fl_dart_project.h new file mode 100644 index 0000000000000..1a379713ccc06 --- /dev/null +++ b/shell/platform/linux/public/flutter_linux/fl_dart_project.h @@ -0,0 +1,91 @@ +// 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_SHELL_PLATFORM_LINUX_FL_DART_PROJECT_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_DART_PROJECT_H_ + +#include + +#if !defined(__FLUTTER_LINUX_INSIDE__) && !defined(FLUTTER_LINUX_COMPILATION) +#error "Only can be included directly." +#endif + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlDartProject, fl_dart_project, FL, DART_PROJECT, GObject) + +/** + * FlDartProject: + * + * #FlDartProject represents a Dart project. It is used to provide information + * about the application when creating a #FlView. + */ + +/** + * fl_dart_project_new: + * @path: a file path, e.g. "my_dart_project" + * + * Create a Flutter project. The project path should contain the following + * top-level items: + * - icudtl.dat (provided as a resource by the Flutter tool) + * - flutter_assets (as built by the Flutter tool) + * + * The path can either be absolute, or relative to the directory containing the + * running executable. + * + * Returns: a new #FlDartProject + */ + +/** + * fl_dart_project_new: + * @path: a file path, e.g. "my_dart_project" + * + * Creates a Flutter project. The project path should contain the following + * top-level items: + * - icudtl.dat (provided as a resource by the Flutter tool) + * - flutter_assets (as built by the Flutter tool) + * + * The path can either be absolute, or relative to the directory containing the + * running executable. + * + * Returns: a new #FlDartProject + */ +FlDartProject* fl_dart_project_new(const gchar* path); + +/** + * fl_dart_project_get_path: + * @project: a #FlDartProject + * + * Gets the path to the directory containing the Flutter application. + * + * Returns: (type filename): a file path, e.g. "/projects/my_dart_project" + */ +const gchar* fl_dart_project_get_path(FlDartProject* project); + +/** + * fl_dart_project_get_assets_path: + * @project: a #FlDartProject + * + * Gets the path to the directory containing the assets used in the Flutter + * application. + * + * Returns: (type filename): a file path, e.g. + * "/projects/my_dart_project/assets" + */ +gchar* fl_dart_project_get_assets_path(FlDartProject* project); + +/** + * fl_dart_project_get_icu_data_path: + * @project: a #FlDartProject + * + * Gets the path to the ICU data file in the Flutter application. + * + * Returns: (type filename): a file path, e.g. + * "/projects/my_dart_project/icudtl.dat" + */ +gchar* fl_dart_project_get_icu_data_path(FlDartProject* project); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_DART_PROJECT_H_ diff --git a/shell/platform/linux/public/flutter_linux/fl_view.h b/shell/platform/linux/public/flutter_linux/fl_view.h new file mode 100644 index 0000000000000..6b93daebfb352 --- /dev/null +++ b/shell/platform/linux/public/flutter_linux/fl_view.h @@ -0,0 +1,38 @@ +// 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_SHELL_PLATFORM_LINUX_FL_VIEW_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_H_ + +#if !defined(__FLUTTER_LINUX_INSIDE__) && !defined(FLUTTER_LINUX_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +#include "fl_dart_project.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlView, fl_view, FL, VIEW, GtkWidget) + +/** + * FlView: + * + * #FlView is a GTK widget that is capable of displaying a Flutter application. + */ + +/** + * fl_view_new: + * @project: The project to show. + * + * Creates a widget to show Flutter application. + * + * Returns: a new #FlView + */ +FlView* fl_view_new(FlDartProject* project); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_H_ diff --git a/shell/platform/linux/public/flutter_linux/flutter_linux.h b/shell/platform/linux/public/flutter_linux/flutter_linux.h new file mode 100644 index 0000000000000..b63b05c24a073 --- /dev/null +++ b/shell/platform/linux/public/flutter_linux/flutter_linux.h @@ -0,0 +1,15 @@ +// 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_SHELL_PLATFORM_LINUX_FLUTTER_LINUX_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FLUTTER_LINUX_H_ + +#define __FLUTTER_LINUX_INSIDE__ + +#include +#include + +#undef __FLUTTER_LINUX_INSIDE__ + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FLUTTER_LINUX_H_ diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index fee227ea7c9e0..5de1a712a482e 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -60,8 +60,6 @@ source_set("flutter_windows_source") { "window_state.h", ] - defines = [ "USE_RAPID_JSON" ] - configs += [ "//flutter/shell/platform/common/cpp:desktop_library_implementation", "//third_party/angle:gl_prototypes", @@ -125,11 +123,6 @@ executable("flutter_windows_unittests") { ":flutter_windows_headers", ":flutter_windows_source", "//flutter/testing", - - # TODO(chunhtai): Consider refactoring flutter_root/testing so that there's a testing - # target that doesn't require a Dart runtime to be linked in. - # https://github.com/flutter/flutter/issues/41414. - "//third_party/dart/runtime:libdart_jit", "//third_party/rapidjson", ] } diff --git a/shell/platform/windows/client_wrapper/BUILD.gn b/shell/platform/windows/client_wrapper/BUILD.gn index c697578f0d60b..f92640e80f7f7 100644 --- a/shell/platform/windows/client_wrapper/BUILD.gn +++ b/shell/platform/windows/client_wrapper/BUILD.gn @@ -6,6 +6,7 @@ import("//flutter/shell/platform/common/cpp/client_wrapper/publish.gni") import("//flutter/testing/testing.gni") _wrapper_includes = [ + "include/flutter/dart_project.h", "include/flutter/flutter_view_controller.h", "include/flutter/flutter_view.h", "include/flutter/plugin_registrar_windows.h", @@ -69,6 +70,7 @@ executable("client_wrapper_windows_unittests") { testonly = true sources = [ + "dart_project_unittests.cc", "flutter_view_controller_unittests.cc", "flutter_view_unittests.cc", "plugin_registrar_windows_unittests.cc", diff --git a/shell/platform/windows/client_wrapper/dart_project_unittests.cc b/shell/platform/windows/client_wrapper/dart_project_unittests.cc new file mode 100644 index 0000000000000..e6d6c17430e0c --- /dev/null +++ b/shell/platform/windows/client_wrapper/dart_project_unittests.cc @@ -0,0 +1,46 @@ +// 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 +#include + +#include "flutter/shell/platform/windows/client_wrapper/include/flutter/dart_project.h" +#include "gtest/gtest.h" + +namespace flutter { + +class DartProjectTest : public ::testing::Test { + protected: + // Wrapper for accessing private icu_data_path. + std::wstring GetProjectIcuDataPath(const DartProject& project) { + return project.icu_data_path(); + } + + // Wrapper for accessing private assets_path. + std::wstring GetProjectAssetsPath(const DartProject& project) { + return project.assets_path(); + } + + // Wrapper for accessing private engine_switches. + std::vector GetProjectEngineSwitches( + const DartProject& project) { + return project.engine_switches(); + } +}; + +TEST_F(DartProjectTest, StandardProjectFormat) { + DartProject project(L"test"); + EXPECT_EQ(GetProjectIcuDataPath(project), L"test\\icudtl.dat"); + EXPECT_EQ(GetProjectAssetsPath(project), L"test\\flutter_assets"); +} + +TEST_F(DartProjectTest, Switches) { + DartProject project(L"test"); + std::vector switches = {"--foo", "--bar"}; + project.SetEngineSwitches(switches); + EXPECT_EQ(GetProjectEngineSwitches(project).size(), 2); + EXPECT_EQ(GetProjectEngineSwitches(project)[0], "--foo"); +} + +} // namespace flutter diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller.cc b/shell/platform/windows/client_wrapper/flutter_view_controller.cc index 92a853862d77c..a16a0699dd188 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller.cc @@ -9,13 +9,35 @@ namespace flutter { +FlutterViewController::FlutterViewController(int width, + int height, + const DartProject& project) { + std::vector switches; + std::transform( + project.engine_switches().begin(), project.engine_switches().end(), + std::back_inserter(switches), + [](const std::string& arg) -> const char* { return arg.c_str(); }); + size_t switch_count = switches.size(); + + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = project.assets_path().c_str(); + properties.icu_data_path = project.icu_data_path().c_str(); + properties.switches = switch_count > 0 ? switches.data() : nullptr; + properties.switches_count = switch_count; + controller_ = FlutterDesktopCreateViewController(width, height, properties); + if (!controller_) { + std::cerr << "Failed to create view controller." << std::endl; + return; + } + view_ = std::make_unique(FlutterDesktopGetView(controller_)); +} + FlutterViewController::FlutterViewController( const std::string& icu_data_path, int width, int height, const std::string& assets_path, - const std::vector& arguments) - : icu_data_path_(icu_data_path) { + const std::vector& arguments) { if (controller_) { std::cerr << "Only one Flutter view can exist at a time." << std::endl; } @@ -26,8 +48,8 @@ FlutterViewController::FlutterViewController( [](const std::string& arg) -> const char* { return arg.c_str(); }); size_t arg_count = engine_arguments.size(); - controller_ = FlutterDesktopCreateViewController( - width, height, assets_path.c_str(), icu_data_path_.c_str(), + controller_ = FlutterDesktopCreateViewControllerLegacy( + width, height, assets_path.c_str(), icu_data_path.c_str(), arg_count > 0 ? &engine_arguments[0] : nullptr, arg_count); if (!controller_) { std::cerr << "Failed to create view controller." << std::endl; diff --git a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc index 1ebd9683ca3d9..ec1b7fb561eae 100644 --- a/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_view_controller_unittests.cc @@ -16,19 +16,16 @@ namespace { // Stub implementation to validate calls to the API. class TestWindowsApi : public testing::StubFlutterWindowsApi { FlutterDesktopViewControllerRef CreateViewController( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) override { + int width, + int height, + const FlutterDesktopEngineProperties& engine_properties) override { return reinterpret_cast(1); } }; } // namespace -TEST(FlutterViewControllerTest, CreateDestroy) { +TEST(FlutterViewControllerTest, CreateDestroyLegacy) { testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); @@ -38,13 +35,20 @@ TEST(FlutterViewControllerTest, CreateDestroy) { } } +TEST(FlutterViewControllerTest, CreateDestroy) { + DartProject project(L"data"); + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + { FlutterViewController controller(100, 100, project); } +} + TEST(FlutterViewControllerTest, GetView) { - std::string icu_data_path = "fake_path_to_icu"; + DartProject project(L"data"); testing::ScopedStubFlutterWindowsApi scoped_api_stub( std::make_unique()); auto test_api = static_cast(scoped_api_stub.stub()); - FlutterViewController controller("", 100, 100, "", - std::vector{}); + FlutterViewController controller(100, 100, project); EXPECT_NE(controller.view(), nullptr); } diff --git a/shell/platform/windows/client_wrapper/include/flutter/dart_project.h b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h new file mode 100644 index 0000000000000..3595239ebd51d --- /dev/null +++ b/shell/platform/windows/client_wrapper/include/flutter/dart_project.h @@ -0,0 +1,65 @@ +// 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_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_DART_PROJECT_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_DART_PROJECT_H_ + +#include +#include + +namespace flutter { + +// A set of Flutter and Dart assets used to initialize a Flutter engine. +class DartProject { + public: + // Creates a DartProject from a directory path. The directory should contain + // the following top-level items: + // - icudtl.dat (provided as a resource by the Flutter tool) + // - flutter_assets (as built by the Flutter tool) + // + // The path can either be absolute, or relative to the directory containing + // the running executable. + explicit DartProject(const std::wstring& path) { + assets_path_ = path + L"\\flutter_assets"; + icu_data_path_ = path + L"\\icudtl.dat"; + } + + ~DartProject() = default; + + // Switches to pass to the Flutter engine. See + // https://github.com/flutter/engine/blob/master/shell/common/switches.h + // for details. Not all switches will apply to embedding mode. Switches have + // not stability guarantee, and are subject to change without notice. + // + // Note: This WILL BE REMOVED in the future. If you call this, please see + // https://github.com/flutter/flutter/issues/38569. + void SetEngineSwitches(const std::vector& switches) { + engine_switches_ = switches; + } + + private: + // Accessors for internals are private, so that they can be changed if more + // flexible options for project structures are needed later without it + // being a breaking change. Provide access to internal classes that need + // them. + friend class FlutterViewController; + friend class DartProjectTest; + + const std::wstring& assets_path() const { return assets_path_; } + const std::wstring& icu_data_path() const { return icu_data_path_; } + const std::vector& engine_switches() const { + return engine_switches_; + } + + // The path to the assets directory. + std::wstring assets_path_; + // The path to the ICU data. + std::wstring icu_data_path_; + // Switches to pass to the engine. + std::vector engine_switches_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CLIENT_WRAPPER_INCLUDE_FLUTTER_DART_PROJECT_H_ diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h index c72b285227240..607e01a72f247 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_view_controller.h @@ -11,6 +11,7 @@ #include #include +#include "dart_project.h" #include "flutter_view.h" #include "plugin_registrar.h" #include "plugin_registry.h" @@ -24,20 +25,15 @@ namespace flutter { // methods in the C API directly, as this class will do that internally. class FlutterViewController : public PluginRegistry { public: - // There must be only one instance of this class in an application at any - // given time, as Flutter does not support multiple engines in one process, - // or multiple views in one engine. - // Creates a FlutterView that can be parented into a Windows View hierarchy // either using HWNDs or in the future into a CoreWindow, or using compositor. - - // The |assets_path| is the path to the flutter_assets folder for the Flutter - // application to be run. |icu_data_path| is the path to the icudtl.dat file - // for the version of Flutter you are using. // - // The |arguments| are passed to the Flutter engine. See: - // https://github.com/flutter/engine/blob/master/shell/common/switches.h for - // for details. Not all arguments will apply to desktop. + // |dart_project| will be used to configure the engine backing this view. + explicit FlutterViewController(int width, + int height, + const DartProject& project); + + // DEPRECATED. Will be removed soon; use the version above. explicit FlutterViewController(const std::string& icu_data_path, int width, int height, @@ -65,10 +61,6 @@ class FlutterViewController : public PluginRegistry { const std::string& plugin_name) override; private: - // The path to the ICU data file. Set at creation time since it is the same - // for any view created. - std::string icu_data_path_; - // Handle for interacting with the C API's view controller, if any. FlutterDesktopViewControllerRef controller_ = nullptr; diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index 07dfb8bd7bbbf..b84d9195daf26 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -36,6 +36,17 @@ ScopedStubFlutterWindowsApi::~ScopedStubFlutterWindowsApi() { // Forwarding dummy implementations of the C API. FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( + int width, + int height, + const FlutterDesktopEngineProperties& engine_properties) { + if (s_stub_implementation) { + return s_stub_implementation->CreateViewController(width, height, + engine_properties); + } + return nullptr; +} + +FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( int initial_width, int initial_height, const char* assets_path, @@ -43,9 +54,11 @@ FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( const char** arguments, size_t argument_count) { if (s_stub_implementation) { + // This stub will be removed shortly, and the current tests don't need the + // arguments, so there's no need to translate them to engine_properties. + FlutterDesktopEngineProperties engine_properties; return s_stub_implementation->CreateViewController( - initial_width, initial_height, assets_path, icu_data_path, arguments, - argument_count); + initial_width, initial_height, engine_properties); } return nullptr; } @@ -78,13 +91,10 @@ HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef controller) { return reinterpret_cast(-1); } -FlutterDesktopEngineRef FlutterDesktopRunEngine(const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { +FlutterDesktopEngineRef FlutterDesktopRunEngine( + const FlutterDesktopEngineProperties& engine_properties) { if (s_stub_implementation) { - return s_stub_implementation->RunEngine(assets_path, icu_data_path, - arguments, argument_count); + return s_stub_implementation->RunEngine(engine_properties); } return nullptr; } diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 0fd74c5b275f5..631d445802f28 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -29,14 +29,11 @@ class StubFlutterWindowsApi { virtual ~StubFlutterWindowsApi() {} - // Called for FlutterDesktopCreateView. + // Called for FlutterDesktopCreateViewController. virtual FlutterDesktopViewControllerRef CreateViewController( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { + int width, + int height, + const FlutterDesktopEngineProperties& engine_properties) { return nullptr; } @@ -50,10 +47,8 @@ class StubFlutterWindowsApi { virtual HWND ViewGetHWND() { return reinterpret_cast(1); } // Called for FlutterDesktopRunEngine. - virtual FlutterDesktopEngineRef RunEngine(const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { + virtual FlutterDesktopEngineRef RunEngine( + const FlutterDesktopEngineProperties& engine_properties) { return nullptr; } diff --git a/shell/platform/windows/flutter_windows.cc b/shell/platform/windows/flutter_windows.cc index 05f8dcc68aec9..c53b055fffeb9 100644 --- a/shell/platform/windows/flutter_windows.cc +++ b/shell/platform/windows/flutter_windows.cc @@ -9,11 +9,13 @@ #include #include #include +#include #include #include #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" +#include "flutter/shell/platform/common/cpp/path_utils.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/windows/dpi_utils.h" #include "flutter/shell/platform/windows/key_event_handler.h" @@ -36,18 +38,16 @@ static_assert(FLUTTER_ENGINE_VERSION == 1, ""); // engine. static std::unique_ptr RunFlutterEngine( flutter::Win32FlutterWindow* window, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t arguments_count) { + const FlutterDesktopEngineProperties& engine_properties) { auto state = std::make_unique(); // FlutterProjectArgs is expecting a full argv, so when processing it for // flags the first item is treated as the executable and ignored. Add a dummy // value so that all provided arguments are used. std::vector argv = {"placeholder"}; - if (arguments_count > 0) { - argv.insert(argv.end(), &arguments[0], &arguments[arguments_count]); + if (engine_properties.switches_count > 0) { + argv.insert(argv.end(), &engine_properties.switches[0], + &engine_properties.switches[engine_properties.switches_count]); } window->CreateRenderSurface(); @@ -105,10 +105,28 @@ static std::unique_ptr RunFlutterEngine( custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); custom_task_runners.platform_task_runner = &platform_task_runner; + std::filesystem::path assets_path(engine_properties.assets_path); + std::filesystem::path icu_path(engine_properties.icu_data_path); + if (assets_path.is_relative() || icu_path.is_relative()) { + // Treat relative paths as relative to the directory of this executable. + std::filesystem::path executable_location = + flutter::GetExecutableDirectory(); + if (executable_location.empty()) { + std::cerr + << "Unable to find executable location to resolve resource paths." + << std::endl; + return nullptr; + } + assets_path = std::filesystem::path(executable_location) / assets_path; + icu_path = std::filesystem::path(executable_location) / icu_path; + } + std::string assets_path_string = assets_path.u8string(); + std::string icu_path_string = icu_path.u8string(); + FlutterProjectArgs args = {}; args.struct_size = sizeof(FlutterProjectArgs); - args.assets_path = assets_path; - args.icu_data_path = icu_data_path; + args.assets_path = assets_path_string.c_str(); + args.icu_data_path = icu_path_string.c_str(); args.command_line_argc = static_cast(argv.size()); args.command_line_argv = &argv[0]; args.platform_message_callback = @@ -132,18 +150,13 @@ static std::unique_ptr RunFlutterEngine( } FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( - int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { + int width, + int height, + const FlutterDesktopEngineProperties& engine_properties) { FlutterDesktopViewControllerRef state = - flutter::Win32FlutterWindow::CreateWin32FlutterWindow(initial_width, - initial_height); + flutter::Win32FlutterWindow::CreateWin32FlutterWindow(width, height); - auto engine_state = RunFlutterEngine( - state->view.get(), assets_path, icu_data_path, arguments, argument_count); + auto engine_state = RunFlutterEngine(state->view.get(), engine_properties); if (!engine_state) { return nullptr; @@ -153,6 +166,26 @@ FlutterDesktopViewControllerRef FlutterDesktopCreateViewController( return state; } +FlutterDesktopViewControllerRef FlutterDesktopCreateViewControllerLegacy( + int initial_width, + int initial_height, + const char* assets_path, + const char* icu_data_path, + const char** arguments, + size_t argument_count) { + std::filesystem::path assets_path_fs = std::filesystem::u8path(assets_path); + std::filesystem::path icu_data_path_fs = + std::filesystem::u8path(icu_data_path); + FlutterDesktopEngineProperties engine_properties = {}; + engine_properties.assets_path = assets_path_fs.c_str(); + engine_properties.icu_data_path = icu_data_path_fs.c_str(); + engine_properties.switches = arguments; + engine_properties.switches_count = argument_count; + + return FlutterDesktopCreateViewController(initial_width, initial_height, + engine_properties); +} + uint64_t FlutterDesktopProcessMessages( FlutterDesktopViewControllerRef controller) { return controller->engine_state->task_runner->ProcessTasks().count(); @@ -191,12 +224,9 @@ UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor) { return flutter::GetDpiForMonitor(monitor); } -FlutterDesktopEngineRef FlutterDesktopRunEngine(const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count) { - auto engine = RunFlutterEngine(nullptr, assets_path, icu_data_path, arguments, - argument_count); +FlutterDesktopEngineRef FlutterDesktopRunEngine( + const FlutterDesktopEngineProperties& engine_properties) { + auto engine = RunFlutterEngine(nullptr, engine_properties); return engine.release(); } @@ -218,6 +248,12 @@ FlutterDesktopMessengerRef FlutterDesktopRegistrarGetMessenger( return registrar->messenger.get(); } +void FlutterDesktopRegistrarSetDestructionHandler( + FlutterDesktopPluginRegistrarRef registrar, + FlutterDesktopOnRegistrarDestroyed callback) { + registrar->destruction_handler = callback; +} + FlutterDesktopViewRef FlutterDesktopRegistrarGetView( FlutterDesktopPluginRegistrarRef registrar) { return registrar->window; diff --git a/shell/platform/windows/key_event_handler.cc b/shell/platform/windows/key_event_handler.cc index 63af2446b889b..8079f197cf333 100644 --- a/shell/platform/windows/key_event_handler.cc +++ b/shell/platform/windows/key_event_handler.cc @@ -8,20 +8,81 @@ #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_message_codec.h" +#include "flutter/shell/platform/common/cpp/json_message_codec.h" static constexpr char kChannelName[] = "flutter/keyevent"; static constexpr char kKeyCodeKey[] = "keyCode"; +static constexpr char kScanCodeKey[] = "scanCode"; +static constexpr char kCharacterCodePointKey[] = "characterCodePoint"; +static constexpr char kModifiersKey[] = "modifiers"; static constexpr char kKeyMapKey[] = "keymap"; static constexpr char kTypeKey[] = "type"; -static constexpr char kAndroidKeyMap[] = "android"; +static constexpr char kWindowsKeyMap[] = "windows"; static constexpr char kKeyUp[] = "keyup"; static constexpr char kKeyDown[] = "keydown"; namespace flutter { +// Re-definition of the modifiers for compatibility with the Flutter framework. +// These have to be in sync with the framework's RawKeyEventDataWindows +// modifiers definition. +// https://github.com/flutter/flutter/blob/19ff596979e407c484a32f4071420fca4f4c885f/packages/flutter/lib/src/services/raw_keyboard_windows.dart#L203 +const int kShift = 1 << 0; +const int kShiftLeft = 1 << 1; +const int kShiftRight = 1 << 2; +const int kControl = 1 << 3; +const int kControlLeft = 1 << 4; +const int kControlRight = 1 << 5; +const int kAlt = 1 << 6; +const int kAltLeft = 1 << 7; +const int kAltRight = 1 << 8; +const int kWinLeft = 1 << 9; +const int kWinRight = 1 << 10; +const int kCapsLock = 1 << 11; +const int kNumLock = 1 << 12; +const int kScrollLock = 1 << 13; + +namespace { +/// Calls GetKeyState() an all modifier keys and packs the result in an int, +/// with the re-defined values declared above for compatibility with the Flutter +/// framework. +int GetModsForKeyState() { + int mods = 0; + + if (GetKeyState(VK_SHIFT) < 0) + mods |= kShift; + if (GetKeyState(VK_LSHIFT) < 0) + mods |= kShiftLeft; + if (GetKeyState(VK_RSHIFT) < 0) + mods |= kShiftRight; + if (GetKeyState(VK_CONTROL) < 0) + mods |= kControl; + if (GetKeyState(VK_LCONTROL) < 0) + mods |= kControlLeft; + if (GetKeyState(VK_RCONTROL) < 0) + mods |= kControlRight; + if (GetKeyState(VK_MENU) < 0) + mods |= kAlt; + if (GetKeyState(VK_LMENU) < 0) + mods |= kAltLeft; + if (GetKeyState(VK_RMENU) < 0) + mods |= kAltRight; + if (GetKeyState(VK_LWIN) < 0) + mods |= kWinLeft; + if (GetKeyState(VK_RWIN) < 0) + mods |= kWinRight; + if (GetKeyState(VK_CAPITAL) < 0) + mods |= kCapsLock; + if (GetKeyState(VK_NUMLOCK) < 0) + mods |= kNumLock; + if (GetKeyState(VK_SCROLL) < 0) + mods |= kScrollLock; + return mods; +} +} // namespace + KeyEventHandler::KeyEventHandler(flutter::BinaryMessenger* messenger) : channel_( std::make_unique>( @@ -38,13 +99,16 @@ void KeyEventHandler::KeyboardHook(Win32FlutterWindow* window, int key, int scancode, int action, - int mods) { + char32_t character) { // TODO: Translate to a cross-platform key code system rather than passing // the native key code. rapidjson::Document event(rapidjson::kObjectType); auto& allocator = event.GetAllocator(); event.AddMember(kKeyCodeKey, key, allocator); - event.AddMember(kKeyMapKey, kAndroidKeyMap, allocator); + event.AddMember(kScanCodeKey, scancode, allocator); + event.AddMember(kCharacterCodePointKey, character, allocator); + event.AddMember(kKeyMapKey, kWindowsKeyMap, allocator); + event.AddMember(kModifiersKey, GetModsForKeyState(), allocator); switch (action) { case WM_KEYDOWN: diff --git a/shell/platform/windows/key_event_handler.h b/shell/platform/windows/key_event_handler.h index 47ec63f6d1755..1b28e90b71787 100644 --- a/shell/platform/windows/key_event_handler.h +++ b/shell/platform/windows/key_event_handler.h @@ -31,7 +31,7 @@ class KeyEventHandler : public KeyboardHookHandler { int key, int scancode, int action, - int mods) override; + char32_t character) override; // |KeyboardHookHandler| void CharHook(Win32FlutterWindow* window, char32_t code_point) override; diff --git a/shell/platform/windows/keyboard_hook_handler.h b/shell/platform/windows/keyboard_hook_handler.h index 1b3304ddd50b0..4583b2bb76d3b 100644 --- a/shell/platform/windows/keyboard_hook_handler.h +++ b/shell/platform/windows/keyboard_hook_handler.h @@ -21,7 +21,7 @@ class KeyboardHookHandler { int key, int scancode, int action, - int mods) = 0; + char32_t character) = 0; // A function for hooking into unicode code point input. virtual void CharHook(Win32FlutterWindow* window, char32_t code_point) = 0; diff --git a/shell/platform/windows/platform_handler.cc b/shell/platform/windows/platform_handler.cc index c4bf592744345..f1540a9819016 100644 --- a/shell/platform/windows/platform_handler.cc +++ b/shell/platform/windows/platform_handler.cc @@ -8,7 +8,7 @@ #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h" +#include "flutter/shell/platform/common/cpp/json_method_codec.h" static constexpr char kChannelName[] = "flutter/platform"; diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index 7626be1239c35..7d9c4155ad32a 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -28,24 +28,48 @@ typedef struct FlutterDesktopView* FlutterDesktopViewRef; // Opaque reference to a Flutter engine instance. typedef struct FlutterDesktopEngineState* FlutterDesktopEngineRef; -// Creates a View running a Flutter Application. +// Properties for configuring a Flutter engine instance. +typedef struct { + // The path to the flutter_assets folder for the application to be run. + // This can either be an absolute path or a path relative to the directory + // containing the executable. + const wchar_t* assets_path; + + // The path to the icudtl.dat file for the version of Flutter you are using. + // This can either be an absolute path or a path relative to the directory + // containing the executable. + const wchar_t* icu_data_path; + + // The switches to pass to the Flutter engine. + // + // See: https://github.com/flutter/engine/blob/master/shell/common/switches.h + // for details. Not all arguments will apply to desktop. + const char** switches; + + // The number of elements in |switches|. + size_t switches_count; +} FlutterDesktopEngineProperties; + +// Creates a View with the given dimensions running a Flutter Application. // -// The |assets_path| is the path to the flutter_assets folder for the Flutter -// application to be run. |icu_data_path| is the path to the icudtl.dat file -// for the version of Flutter you are using. -// -// The |arguments| are passed to the Flutter engine. See: -// https://github.com/flutter/engine/blob/master/shell/common/switches.h for -// for details. Not all arguments will apply to desktop. +// This will set up and run an associated Flutter engine using the settings in +// |engine_properties|. // // Returns a null pointer in the event of an error. FLUTTER_EXPORT FlutterDesktopViewControllerRef -FlutterDesktopCreateViewController(int initial_width, - int initial_height, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count); +FlutterDesktopCreateViewController( + int width, + int height, + const FlutterDesktopEngineProperties& engine_properties); + +// DEPRECATED. Will be removed soon; switch to the version above. +FLUTTER_EXPORT FlutterDesktopViewControllerRef +FlutterDesktopCreateViewControllerLegacy(int initial_width, + int initial_height, + const char* assets_path, + const char* icu_data_path, + const char** arguments, + size_t argument_count); // Shuts down the engine instance associated with |controller|, and cleans up // associated state. @@ -88,20 +112,9 @@ FLUTTER_EXPORT UINT FlutterDesktopGetDpiForMonitor(HMONITOR monitor); // Runs an instance of a headless Flutter engine. // -// The |assets_path| is the path to the flutter_assets folder for the Flutter -// application to be run. |icu_data_path| is the path to the icudtl.dat file -// for the version of Flutter you are using. -// -// The |arguments| are passed to the Flutter engine. See: -// https://github.com/flutter/engine/blob/master/shell/common/switches.h for -// for details. Not all arguments will apply to desktop. -// // Returns a null pointer in the event of an error. -FLUTTER_EXPORT FlutterDesktopEngineRef -FlutterDesktopRunEngine(const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t argument_count); +FLUTTER_EXPORT FlutterDesktopEngineRef FlutterDesktopRunEngine( + const FlutterDesktopEngineProperties& engine_properties); // Shuts down the given engine instance. Returns true if the shutdown was // successful. |engine_ref| is no longer valid after this call. diff --git a/shell/platform/windows/testing/win32_window_test.cc b/shell/platform/windows/testing/win32_window_test.cc index 13a70e039647c..53ad12b5b71d9 100644 --- a/shell/platform/windows/testing/win32_window_test.cc +++ b/shell/platform/windows/testing/win32_window_test.cc @@ -21,7 +21,10 @@ void Win32WindowTest::OnPointerLeave() {} void Win32WindowTest::OnChar(char32_t code_point) {} -void Win32WindowTest::OnKey(int key, int scancode, int action, int mods) {} +void Win32WindowTest::OnKey(int key, + int scancode, + int action, + char32_t character) {} void Win32WindowTest::OnScroll(double delta_x, double delta_y) {} diff --git a/shell/platform/windows/testing/win32_window_test.h b/shell/platform/windows/testing/win32_window_test.h index ccefdfe717dbf..77d25464b9e9a 100644 --- a/shell/platform/windows/testing/win32_window_test.h +++ b/shell/platform/windows/testing/win32_window_test.h @@ -45,7 +45,7 @@ class Win32WindowTest : public Win32Window { void OnChar(char32_t code_point) override; // |Win32Window| - void OnKey(int key, int scancode, int action, int mods) override; + void OnKey(int key, int scancode, int action, char32_t character) override; // |Win32Window| void OnScroll(double delta_x, double delta_y) override; diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index 6b71ff23e5c3d..10b3864d4fcdf 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -9,7 +9,7 @@ #include #include -#include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/json_method_codec.h" +#include "flutter/shell/platform/common/cpp/json_method_codec.h" static constexpr char kSetEditingStateMethod[] = "TextInput.setEditingState"; static constexpr char kClearClientMethod[] = "TextInput.clearClient"; @@ -49,7 +49,7 @@ void TextInputPlugin::KeyboardHook(Win32FlutterWindow* window, int key, int scancode, int action, - int mods) { + char32_t character) { if (active_model_ == nullptr) { return; } diff --git a/shell/platform/windows/text_input_plugin.h b/shell/platform/windows/text_input_plugin.h index 03cf9b7c5547b..dced9a69545eb 100644 --- a/shell/platform/windows/text_input_plugin.h +++ b/shell/platform/windows/text_input_plugin.h @@ -32,7 +32,7 @@ class TextInputPlugin : public KeyboardHookHandler { int key, int scancode, int action, - int mods) override; + char32_t character) override; // |KeyboardHookHandler| void CharHook(Win32FlutterWindow* window, char32_t code_point) override; diff --git a/shell/platform/windows/win32_flutter_window.cc b/shell/platform/windows/win32_flutter_window.cc index c0c786799285d..73c46febf3f7b 100644 --- a/shell/platform/windows/win32_flutter_window.cc +++ b/shell/platform/windows/win32_flutter_window.cc @@ -15,6 +15,9 @@ Win32FlutterWindow::Win32FlutterWindow(int width, int height) { Win32FlutterWindow::~Win32FlutterWindow() { DestroyRenderSurface(); + if (plugin_registrar_ && plugin_registrar_->destruction_handler) { + plugin_registrar_->destruction_handler(plugin_registrar_.get()); + } } FlutterDesktopViewControllerRef Win32FlutterWindow::CreateWin32FlutterWindow( @@ -164,9 +167,12 @@ void Win32FlutterWindow::OnChar(char32_t code_point) { } } -void Win32FlutterWindow::OnKey(int key, int scancode, int action, int mods) { +void Win32FlutterWindow::OnKey(int key, + int scancode, + int action, + char32_t character) { if (process_events_) { - SendKey(key, scancode, action, 0); + SendKey(key, scancode, action, character); } } @@ -264,9 +270,12 @@ void Win32FlutterWindow::SendChar(char32_t code_point) { } } -void Win32FlutterWindow::SendKey(int key, int scancode, int action, int mods) { +void Win32FlutterWindow::SendKey(int key, + int scancode, + int action, + char32_t character) { for (const auto& handler : keyboard_hook_handlers_) { - handler->KeyboardHook(this, key, scancode, action, mods); + handler->KeyboardHook(this, key, scancode, action, character); } } diff --git a/shell/platform/windows/win32_flutter_window.h b/shell/platform/windows/win32_flutter_window.h index 5fa58aa295c8c..bce1f2e6fd644 100644 --- a/shell/platform/windows/win32_flutter_window.h +++ b/shell/platform/windows/win32_flutter_window.h @@ -60,7 +60,7 @@ class Win32FlutterWindow : public Win32Window { void OnChar(char32_t code_point) override; // |Win32Window| - void OnKey(int key, int scancode, int action, int mods) override; + void OnKey(int key, int scancode, int action, char32_t character) override; // |Win32Window| void OnScroll(double delta_x, double delta_y) override; @@ -116,7 +116,7 @@ class Win32FlutterWindow : public Win32Window { void SendChar(char32_t code_point); // Reports a raw keyboard message to Flutter engine. - void SendKey(int key, int scancode, int action, int mods); + void SendKey(int key, int scancode, int action, char32_t character); // Reports scroll wheel events to Flutter engine. void SendScroll(double delta_x, double delta_y); diff --git a/shell/platform/windows/win32_window.cc b/shell/platform/windows/win32_window.cc index 0abafb562fbf1..6f8c48e9c03d6 100644 --- a/shell/platform/windows/win32_window.cc +++ b/shell/platform/windows/win32_window.cc @@ -110,6 +110,7 @@ Win32Window::MessageHandler(HWND hwnd, auto window = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); UINT button_pressed = 0; + if (window != nullptr) { switch (message) { case kWmDpiChangedBeforeParent: @@ -190,35 +191,71 @@ Win32Window::MessageHandler(HWND hwnd, // DefWindowProc will send WM_CHAR for this WM_UNICHAR. break; } + case WM_DEADCHAR: + case WM_SYSDEADCHAR: case WM_CHAR: case WM_SYSCHAR: { - if (wparam == VK_BACK) - break; char32_t code_point = static_cast(wparam); static char32_t lead_surrogate = 0; - // If code_point is LeadSurrogate, save and return. + // If code_point is LeadSurrogate, save to combine to potentially form + // a complex Unicode character. if ((code_point & 0xFFFFFC00) == 0xD800) { lead_surrogate = code_point; - return TRUE; - } - // Merge TrailSurrogate and LeadSurrogate. - if (lead_surrogate != 0 && (code_point & 0xFFFFFC00) == 0xDC00) { + } else if (lead_surrogate != 0 && (code_point & 0xFFFFFC00) == 0xDC00) { + // Merge TrailSurrogate and LeadSurrogate. code_point = 0x10000 + ((lead_surrogate & 0x000003FF) << 10) + (code_point & 0x3FF); + lead_surrogate = 0; + } + + // In an ENG-INTL keyboard, pressing "'" + "e" produces é. In this case, + // the "'" key is a dead char, and shouldn't be sent to window->OnChar + // for text input. However, the key event should still be sent to + // Flutter. The result would be: + // * Key event - key code: 222 (quote) - key label: ' + // * Key event - key code: 69 (e) - key label: é + // + // As for text input, only the second key press will display a + // character. + if (wparam != VK_BACK && message != WM_DEADCHAR && + message != WM_SYSDEADCHAR) { + window->OnChar(code_point); + } + + // All key presses that generate a character should be sent from + // WM_CHAR. In order to send the full key press information, the keycode + // is persisted in keycode_for_char_message_ obtained from WM_KEYDOWN. + if (keycode_for_char_message_ != 0) { + const unsigned int scancode = (lparam >> 16) & 0xff; + window->OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN, + code_point); + keycode_for_char_message_ = 0; } - lead_surrogate = 0; - window->OnChar(code_point); break; } case WM_KEYDOWN: case WM_SYSKEYDOWN: case WM_KEYUP: case WM_SYSKEYUP: - unsigned char scancode = ((unsigned char*)&lparam)[2]; - unsigned int virtualKey = MapVirtualKey(scancode, MAPVK_VSC_TO_VK); - const int key = virtualKey; - const int action = message == WM_KEYDOWN ? WM_KEYDOWN : WM_KEYUP; - window->OnKey(key, scancode, action, 0); + const bool is_keydown_message = + (message == WM_KEYDOWN || message == WM_SYSKEYDOWN); + // Check if this key produces a character. If so, the key press should + // be sent with the character produced at WM_CHAR. Store the produced + // keycode (it's not accessible from WM_CHAR) to be used in WM_CHAR. + const unsigned int character = MapVirtualKey(wparam, MAPVK_VK_TO_CHAR); + if (character > 0 && is_keydown_message) { + keycode_for_char_message_ = wparam; + break; + } + unsigned int keyCode(wparam); + const unsigned int scancode = (lparam >> 16) & 0xff; + // If the key is a modifier, get its side. + if (keyCode == VK_SHIFT || keyCode == VK_MENU || + keyCode == VK_CONTROL) { + keyCode = MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX); + } + const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP; + window->OnKey(keyCode, scancode, action, 0); break; } return DefWindowProc(hwnd, message, wparam, lparam); diff --git a/shell/platform/windows/win32_window.h b/shell/platform/windows/win32_window.h index dfb5928048168..ba959ffcaa3b9 100644 --- a/shell/platform/windows/win32_window.h +++ b/shell/platform/windows/win32_window.h @@ -106,7 +106,7 @@ class Win32Window { virtual void OnChar(char32_t code_point) = 0; // Called when raw keyboard input occurs. - virtual void OnKey(int key, int scancode, int action, int mods) = 0; + virtual void OnKey(int key, int scancode, int action, char32_t character) = 0; // Called when mouse scrollwheel input occurs. virtual void OnScroll(double delta_x, double delta_y) = 0; @@ -172,6 +172,10 @@ class Win32Window { // Keeps track of mouse state in relation to the window. MouseState mouse_state_; + + // Keeps track of the last key code produced by a WM_KEYDOWN or WM_SYSKEYDOWN + // message. + int keycode_for_char_message_ = 0; }; } // namespace flutter diff --git a/shell/platform/windows/window_state.h b/shell/platform/windows/window_state.h index 086f6aebd450f..6f0451d7ec940 100644 --- a/shell/platform/windows/window_state.h +++ b/shell/platform/windows/window_state.h @@ -55,6 +55,9 @@ struct FlutterDesktopPluginRegistrar { // The handle for the window associated with this registrar. FlutterDesktopView* window; + + // Callback to be called on registrar destruction. + FlutterDesktopOnRegistrarDestroyed destruction_handler; }; // State associated with the messenger used to communicate with the engine. diff --git a/shell/testing/BUILD.gn b/shell/testing/BUILD.gn index d7b793320b568..fce083fc12b1b 100644 --- a/shell/testing/BUILD.gn +++ b/shell/testing/BUILD.gn @@ -29,10 +29,6 @@ executable("testing") { ] if (is_fuchsia) { - if (!using_fuchsia_sdk) { - deps += [ "//garnet/public/lib/ui/scenic:client" ] - } else { - deps += [ "$fuchsia_sdk_root/pkg:scenic_cpp" ] - } + deps += [ "$fuchsia_sdk_root/pkg:scenic_cpp" ] } } diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 3f31fd6cefe25..cab64817ae241 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -109,7 +109,7 @@ int RunTester(const flutter::Settings& settings, std::unique_ptr threadhost; fml::RefPtr platform_task_runner; - fml::RefPtr gpu_task_runner; + fml::RefPtr raster_task_runner; fml::RefPtr ui_task_runner; fml::RefPtr io_task_runner; @@ -118,17 +118,17 @@ int RunTester(const flutter::Settings& settings, thread_label, ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI | ThreadHost::Type::GPU); platform_task_runner = current_task_runner; - gpu_task_runner = threadhost->gpu_thread->GetTaskRunner(); + raster_task_runner = threadhost->raster_thread->GetTaskRunner(); ui_task_runner = threadhost->ui_thread->GetTaskRunner(); io_task_runner = threadhost->io_thread->GetTaskRunner(); } else { - platform_task_runner = gpu_task_runner = ui_task_runner = io_task_runner = - current_task_runner; + platform_task_runner = raster_task_runner = ui_task_runner = + io_task_runner = current_task_runner; } const flutter::TaskRunners task_runners(thread_label, // dart thread label platform_task_runner, // platform - gpu_task_runner, // gpu + raster_task_runner, // raster ui_task_runner, // ui io_task_runner // io ); diff --git a/sky/packages/sky_engine/BUILD.gn b/sky/packages/sky_engine/BUILD.gn index d0af03dc96375..9d627c5dbeb5e 100644 --- a/sky/packages/sky_engine/BUILD.gn +++ b/sky/packages/sky_engine/BUILD.gn @@ -229,23 +229,3 @@ dart_pkg("sky_engine") { "$service_isolate_dir/vmservice_server.dart", ] } - -if (is_fuchsia && !using_fuchsia_sdk) { - import("//build/dart/dart_library.gni") - - dart_library("sky_engine_dart") { - package_name = "sky_engine" - - package_root = "$root_gen_dir/dart-pkg/sky_engine" - - sources = [] - - disable_analysis = true - - non_dart_deps = [ - # This will ensure all the source files needed for this package are - # copied to the right location. - ":sky_engine", - ] - } -} diff --git a/sky/packages/sky_engine/LICENSE b/sky/packages/sky_engine/LICENSE index 17d812e9a5e1f..fca14e2785127 100644 --- a/sky/packages/sky_engine/LICENSE +++ b/sky/packages/sky_engine/LICENSE @@ -54,29 +54,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- angle -Copyright (c) 2007-2016 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. --------------------------------------------------------------------------------- -angle - Copyright (c) 2008-2018 The Khronos Group Inc. Permission is hereby granted, free of charge, to any person obtaining a @@ -100,75 +77,6 @@ MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. -------------------------------------------------------------------------------- angle -Copyright (c) 2013-2016 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. --------------------------------------------------------------------------------- -angle - -Copyright (c) 2013-2017 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. --------------------------------------------------------------------------------- -angle - -Copyright (c) 2013-2018 The Khronos Group Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and/or associated documentation files (the -"Materials"), to deal in the Materials without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Materials, and to -permit persons to whom the Materials are furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Materials. - -THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. --------------------------------------------------------------------------------- -angle - Copyright 2002 The ANGLE Project Authors. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -514,6 +422,7 @@ angle boringssl engine etc1 +khronos observatory_pub_packages txt vulkan @@ -753,6 +662,102 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- angle +khronos + +Copyright (c) 2007-2016 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +angle +khronos + +Copyright (c) 2013-2016 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +angle +khronos + +Copyright (c) 2013-2017 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +angle +khronos + +Copyright (c) 2013-2018 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +angle vulkan-validation-layers Copyright 2018 The ANGLE Project Authors. All rights reserved. @@ -4390,6 +4395,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- engine +gpu tonic txt @@ -8621,6 +8627,102 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- khronos +Copyright (c) 2007-2010 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. + +SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008) + +Copyright (C) 1992 Silicon Graphics, Inc. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice including the dates of first publication and either +this permission notice or a reference to http://oss.sgi.com/projects/FreeB +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON +GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Silicon Graphics, Inc. shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Silicon +Graphics, Inc. +-------------------------------------------------------------------------------- +khronos + +Copyright (c) 2007-2012 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +khronos + +Copyright (c) 2008-2009 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. +-------------------------------------------------------------------------------- +khronos + Copyright (c) 2013-2014 The Khronos Group Inc. Permission is hereby granted, free of charge, to any person obtaining a diff --git a/sky/tools/create_ios_framework.py b/sky/tools/create_ios_framework.py index bb74044b546e6..8c79921c589f5 100755 --- a/sky/tools/create_ios_framework.py +++ b/sky/tools/create_ios_framework.py @@ -10,6 +10,9 @@ import os +DSYMUTIL = os.path.join(os.path.dirname(__file__), '..', '..', '..', + 'buildtools', 'mac-x64', 'clang', 'bin', 'dsymutil') + def main(): parser = argparse.ArgumentParser(description='Creates Flutter.framework') @@ -33,27 +36,31 @@ def main(): simulator_dylib = os.path.join(simulator_framework, 'Flutter') if not os.path.isdir(arm64_framework): - print 'Cannot find iOS arm64 Framework at', arm64_framework + print('Cannot find iOS arm64 Framework at %s' % arm64_framework) return 1 if not os.path.isdir(armv7_framework): - print 'Cannot find iOS armv7 Framework at', armv7_framework + print('Cannot find iOS armv7 Framework at %s' % armv7_framework) return 1 if not os.path.isdir(simulator_framework): - print 'Cannot find iOS simulator Framework at', simulator_framework + print('Cannot find iOS simulator Framework at %s' % simulator_framework) return 1 if not os.path.isfile(arm64_dylib): - print 'Cannot find iOS arm64 dylib at', arm64_dylib + print('Cannot find iOS arm64 dylib at %s' % arm64_dylib) return 1 if not os.path.isfile(armv7_dylib): - print 'Cannot find iOS armv7 dylib at', armv7_dylib + print('Cannot find iOS armv7 dylib at %s' % armv7_dylib) return 1 if not os.path.isfile(simulator_dylib): - print 'Cannot find iOS simulator dylib at', simulator_dylib + print('Cannot find iOS simulator dylib at %s' % simulator_dylib) + return 1 + + if not os.path.isfile(DSYMUTIL): + print('Cannot find dsymutil at %s' % DSYMUTIL) return 1 shutil.rmtree(fat_framework, True) @@ -76,7 +83,7 @@ def main(): if args.dsym: dsym_out = os.path.splitext(fat_framework)[0] + '.dSYM' - subprocess.check_call(['dsymutil', '-o', dsym_out, linker_out]) + subprocess.check_call([DSYMUTIL, '-o', dsym_out, linker_out]) if args.strip: # copy unstripped diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 85fbe373e5f3d..25d1ed04dc4d6 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -181,7 +181,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_gradient.png'); expect(areEqual, true); - }); + }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 test('Simple dithered gradient', () async { Paint.enableDithering = true; @@ -197,5 +197,5 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); expect(areEqual, true); - }); + }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 } diff --git a/testing/dart/locale_test.dart b/testing/dart/locale_test.dart index 5f1f83780732a..64c16db78688b 100644 --- a/testing/dart/locale_test.dart +++ b/testing/dart/locale_test.dart @@ -13,8 +13,6 @@ void main() { expect(const Locale('en').toLanguageTag(), 'en'); expect(const Locale('en'), const Locale('en', $null)); expect(const Locale('en').hashCode, const Locale('en', $null).hashCode); - expect(const Locale('en'), isNot(const Locale('en', ''))); - expect(const Locale('en').hashCode, isNot(const Locale('en', '').hashCode)); expect(const Locale('en', 'US').toLanguageTag(), 'en-US'); expect(const Locale('en', 'US').toString(), 'en_US'); expect(const Locale('iw').toLanguageTag(), 'he'); @@ -52,5 +50,21 @@ void main() { isNot(const Locale.fromSubtags(languageCode: 'en', scriptCode: 'Latn'))); expect(const Locale.fromSubtags(languageCode: 'en').hashCode, isNot(const Locale.fromSubtags(languageCode: 'en', scriptCode: 'Latn').hashCode)); + + expect(const Locale('en', ''), const Locale('en')); + expect(const Locale('en'), const Locale('en', '')); + expect(const Locale('en'), const Locale('en')); + expect(const Locale('en', ''), const Locale('en', '')); + + expect(const Locale('en', ''), isNot(const Locale('en', 'GB'))); + expect(const Locale('en'), isNot(const Locale('en', 'GB'))); + expect(const Locale('en', 'GB'), isNot(const Locale('en', ''))); + expect(const Locale('en', 'GB'), isNot(const Locale('en'))); + }); + + test('Locale toString does not include separator for \'\'', () { + expect(const Locale('en').toString(), 'en'); + expect(const Locale('en', '').toString(), 'en'); + expect(const Locale('en', 'US').toString(), 'en_US'); }); } diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 64fd956f19622..17c1bf90890dd 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -19,6 +19,7 @@ import 'dart:typed_data'; import 'package:test/test.dart'; // HACK: these parts are to get access to private functions tested here. +part '../../lib/ui/annotations.dart'; part '../../lib/ui/channel_buffers.dart'; part '../../lib/ui/compositing.dart'; part '../../lib/ui/geometry.dart'; diff --git a/testing/dart_isolate_runner.cc b/testing/dart_isolate_runner.cc index 7212879d18407..8fe1d1f5ee09b 100644 --- a/testing/dart_isolate_runner.cc +++ b/testing/dart_isolate_runner.cc @@ -55,7 +55,7 @@ void RunDartCodeInIsolate(DartVMRef& vm_ref, const std::vector& args, const std::string& fixtures_path, fml::WeakPtr io_manager) { - FML_CHECK(task_runners.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); + FML_CHECK(task_runners.GetUITaskRunner()->RunsTasksOnCurrentThread()); if (!vm_ref) { return; @@ -163,7 +163,7 @@ std::unique_ptr RunDartCodeInIsolate( std::unique_ptr result; fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( - task_runners.GetPlatformTaskRunner(), fml::MakeCopyable([&]() mutable { + task_runners.GetUITaskRunner(), fml::MakeCopyable([&]() mutable { RunDartCodeInIsolate(vm_ref, result, settings, task_runners, entrypoint, args, fixtures_path, io_manager); latch.Signal(); diff --git a/testing/fuchsia/run_tests.sh b/testing/fuchsia/run_tests.sh index 8bbd5be91b990..7fa6d38b68e77 100755 --- a/testing/fuchsia/run_tests.sh +++ b/testing/fuchsia/run_tests.sh @@ -36,14 +36,19 @@ reboot() { trap reboot EXIT +echo "$(date) START:PAVING ------------------------------------------" ./fuchsia_ctl -d $device_name pave -i $1 +echo "$(date) END:PAVING --------------------------------------------" +echo "$(date) START:WAIT_DEVICE_READY -------------------------------" for i in {1..10}; do ./fuchsia_ctl -d $device_name ssh -c "echo up" && break || sleep 15; done +echo "$(date) END:WAIT_DEVICE_READY ---------------------------------" # TODO(gw280): Enable tests using JIT runner +echo "$(date) START:flutter_runner_tests ----------------------------" ./fuchsia_ctl -d $device_name test \ -f flutter_aot_runner-0.far \ -f flutter_runner_tests-0.far \ @@ -58,21 +63,27 @@ done # TODO(https://github.com/flutter/flutter/issues/50032) Enable after the # Fuchsia message loop migration is complete. +echo "$(date) START:fml_tests ---------------------------------------" ./fuchsia_ctl -d $device_name test \ -f fml_tests-0.far \ -t fml_tests \ -a "--gtest_filter=-MessageLoop*:Message*:FileTest*" +echo "$(date) START:flow_tests --------------------------------------" ./fuchsia_ctl -d $device_name test \ -f flow_tests-0.far \ -t flow_tests +echo "$(date) START:runtime_tests -----------------------------------" ./fuchsia_ctl -d $device_name test \ -f runtime_tests-0.far \ -t runtime_tests +# TODO(https://github.com/flutter/flutter/issues/53399): Re-enable +# OnServiceProtocolGetSkSLsWorks and CanLoadSkSLsFromAsset once they pass on +# Fuchsia. +echo "$(date) START:shell_tests -------------------------------------" ./fuchsia_ctl -d $device_name test \ -f shell_tests-0.far \ -t shell_tests \ - -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*" - + -a "--gtest_filter=-ShellTest.CacheSkSLWorks:ShellTest.SetResourceCacheSize*:ShellTest.OnServiceProtocolGetSkSLsWorks:ShellTest.CanLoadSkSLsFromAsset" diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 82f36199ac95a..950e9dbdb03f3 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -7,18 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 0D17A5C022D78FCD0057279F /* FlutterViewControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D17A5BF22D78FCD0057279F /* FlutterViewControllerTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; 0D1CE5D8233430F400E5D880 /* FlutterChannelsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; - 0D4C3FB022DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D4C3FAF22DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; - 0D52D3BD22C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0D52D3B622C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; 0D6AB6B622BB05E100EEE540 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */; }; 0D6AB6B922BB05E100EEE540 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6B822BB05E100EEE540 /* ViewController.m */; }; 0D6AB6BC22BB05E100EEE540 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BA22BB05E100EEE540 /* Main.storyboard */; }; 0D6AB6BE22BB05E200EEE540 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BD22BB05E200EEE540 /* Assets.xcassets */; }; 0D6AB6C122BB05E200EEE540 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BF22BB05E200EEE540 /* LaunchScreen.storyboard */; }; 0D6AB6C422BB05E200EEE540 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6C322BB05E200EEE540 /* main.m */; }; - 0D6AB6EB22BB40E700EEE540 /* FlutterEngineTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6E722BB40CF00EEE540 /* FlutterEngineTest.mm */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; - 0D6AB72C22BC339F00EEE540 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D6AB72522BC336100EEE540 /* libOCMock.a */; }; 0D6AB73F22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */; }; /* End PBXBuildFile section */ @@ -30,55 +25,10 @@ remoteGlobalIDString = 0D6AB6B022BB05E100EEE540; remoteInfo = IosUnitTests; }; - 0D6AB72022BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 030EF0A814632FD000B04273; - remoteInfo = OCMock; - }; - 0D6AB72222BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 03565A3118F0566E003AE91E; - remoteInfo = OCMockTests; - }; - 0D6AB72422BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 030EF0DC14632FF700B04273; - remoteInfo = OCMockLib; - }; - 0D6AB72622BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D31108AD1828DB8700737925; - remoteInfo = OCMockLibTests; - }; - 0D6AB72822BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = F0B950F11B0080BE00942C38; - remoteInfo = "OCMock iOS"; - }; - 0D6AB72A22BC336100EEE540 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 817EB1621BD765130047E85A; - remoteInfo = "OCMock tvOS"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0D17A5BF22D78FCD0057279F /* FlutterViewControllerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerTest.m; sourceTree = ""; }; 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterChannelsTest.m; sourceTree = ""; }; - 0D4C3FAF22DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterPluginAppLifeCycleDelegateTest.m; sourceTree = ""; }; - 0D52D3B622C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterBinaryMessengerRelayTest.mm; sourceTree = ""; }; 0D6AB6B122BB05E100EEE540 /* IosUnitTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IosUnitTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -91,8 +41,6 @@ 0D6AB6C322BB05E200EEE540 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 0D6AB6C922BB05E200EEE540 /* IosUnitTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IosUnitTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6CF22BB05E200EEE540 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 0D6AB6E722BB40CF00EEE540 /* FlutterEngineTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEngineTest.mm; sourceTree = ""; }; - 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCMock.xcodeproj; path = ../../../../../third_party/ocmock/Source/OCMock.xcodeproj; sourceTree = ""; }; 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -108,7 +56,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0D6AB72C22BC339F00EEE540 /* libOCMock.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -163,26 +110,12 @@ isa = PBXGroup; children = ( 0D1CE5D62334309900E5D880 /* Source-Common */, - 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */, - 0D6AB6E622BB409F00EEE540 /* Source */, 0D6AB6CF22BB05E200EEE540 /* Info.plist */, 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */, ); path = Tests; sourceTree = ""; }; - 0D6AB6E622BB409F00EEE540 /* Source */ = { - isa = PBXGroup; - children = ( - 0D52D3B622C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm */, - 0D6AB6E722BB40CF00EEE540 /* FlutterEngineTest.mm */, - 0D17A5BF22D78FCD0057279F /* FlutterViewControllerTest.m */, - 0D4C3FAF22DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m */, - ); - name = Source; - path = ../../../../shell/platform/darwin/ios/framework/Source; - sourceTree = ""; - }; 0D6AB6FC22BC1BC300EEE540 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -190,19 +123,6 @@ name = Frameworks; sourceTree = ""; }; - 0D6AB71822BC336100EEE540 /* Products */ = { - isa = PBXGroup; - children = ( - 0D6AB72122BC336100EEE540 /* OCMock.framework */, - 0D6AB72322BC336100EEE540 /* OCMockTests.xctest */, - 0D6AB72522BC336100EEE540 /* libOCMock.a */, - 0D6AB72722BC336100EEE540 /* OCMockLibTests.xctest */, - 0D6AB72922BC336100EEE540 /* OCMock.framework */, - 0D6AB72B22BC336100EEE540 /* OCMock.framework */, - ); - name = Products; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -230,7 +150,6 @@ 0D6AB6C522BB05E200EEE540 /* Sources */, 0D6AB6C622BB05E200EEE540 /* Frameworks */, 0D6AB6C722BB05E200EEE540 /* Resources */, - 0D6AB6E122BB122F00EEE540 /* ShellScript */, ); buildRules = ( ); @@ -271,12 +190,6 @@ mainGroup = 0D6AB6A822BB05E100EEE540; productRefGroup = 0D6AB6B222BB05E100EEE540 /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 0D6AB71822BC336100EEE540 /* Products */; - ProjectRef = 0D6AB71722BC336100EEE540 /* OCMock.xcodeproj */; - }, - ); projectRoot = ""; targets = ( 0D6AB6B022BB05E100EEE540 /* IosUnitTests */, @@ -285,51 +198,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - 0D6AB72122BC336100EEE540 /* OCMock.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = OCMock.framework; - remoteRef = 0D6AB72022BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D6AB72322BC336100EEE540 /* OCMockTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = OCMockTests.xctest; - remoteRef = 0D6AB72222BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D6AB72522BC336100EEE540 /* libOCMock.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libOCMock.a; - remoteRef = 0D6AB72422BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D6AB72722BC336100EEE540 /* OCMockLibTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = OCMockLibTests.xctest; - remoteRef = 0D6AB72622BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D6AB72922BC336100EEE540 /* OCMock.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = OCMock.framework; - remoteRef = 0D6AB72822BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D6AB72B22BC336100EEE540 /* OCMock.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = OCMock.framework; - remoteRef = 0D6AB72A22BC336100EEE540 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ 0D6AB6AF22BB05E100EEE540 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -351,26 +219,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 0D6AB6E122BB122F00EEE540 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = "/bin/sh -x"; - shellScript = "TARGET_FRAMEWORKS_PATH=\"$TARGET_BUILD_DIR/../Frameworks\"\nFRAMEWORK_PATH=$PROJECT_DIR/../../../../out/$FLUTTER_ENGINE/Flutter.framework\nrsync -avzh \"$FRAMEWORK_PATH\" \"$TARGET_FRAMEWORKS_PATH\"\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 0D6AB6AD22BB05E100EEE540 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -386,11 +234,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0D6AB6EB22BB40E700EEE540 /* FlutterEngineTest.mm in Sources */, - 0D17A5C022D78FCD0057279F /* FlutterViewControllerTest.m in Sources */, 0D1CE5D8233430F400E5D880 /* FlutterChannelsTest.m in Sources */, - 0D52D3BD22C566D50011DEBD /* FlutterBinaryMessengerRelayTest.mm in Sources */, - 0D4C3FB022DF9F5300A67C70 /* FlutterPluginAppLifeCycleDelegateTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -585,7 +429,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_ARC = NO; CODE_SIGN_STYLE = Automatic; - FRAMEWORK_SEARCH_PATHS = ../../../../out/$FLUTTER_ENGINE; + FRAMEWORK_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = ( ../../../.., ../../../../flutter/shell/platform/darwin/common/framework/Headers, @@ -597,20 +441,17 @@ ../../../../third_party/icu/source/common, ); INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = ""; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Tests", + ../../../../out/$FLUTTER_ENGINE, + ../../../../out/$FLUTTER_ENGINE/obj/flutter/shell/platform/darwin/ios/, ); OTHER_LDFLAGS = ( - "-framework", - Flutter, - "-lOCMock", + "-locmock", "-ObjC", + "-lios_test_flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.google.flutter.IosUnitTestsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -629,7 +470,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_ARC = NO; CODE_SIGN_STYLE = Automatic; - FRAMEWORK_SEARCH_PATHS = ../../../../out/$FLUTTER_ENGINE; + FRAMEWORK_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = ( ../../../.., ../../../../flutter/shell/platform/darwin/common/framework/Headers, @@ -641,20 +482,17 @@ ../../../../third_party/icu/source/common, ); INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = ""; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Tests", + ../../../../out/$FLUTTER_ENGINE, + ../../../../out/$FLUTTER_ENGINE/obj/flutter/shell/platform/darwin/ios/, ); OTHER_LDFLAGS = ( - "-framework", - Flutter, - "-lOCMock", + "-locmock", "-ObjC", + "-lios_test_flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.google.flutter.IosUnitTestsTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/testing/ios/IosUnitTests/README.md b/testing/ios/IosUnitTests/README.md new file mode 100644 index 0000000000000..4f9add7806c32 --- /dev/null +++ b/testing/ios/IosUnitTests/README.md @@ -0,0 +1,22 @@ +# iOS Unit Tests + +These are the unit tests for iOS engine. They can be executed locally and are +also run in LUCI builds. + +## Running Tests + +```sh +./flutter/tools/gn --ios --simulator --unoptimized +cd flutter/testing/ios/IosUnitTests +./build_and_run_tests.sh +``` + +After the `ios_flutter_test` target is built you can also run the tests inside +of xcode with `IosUnitTests.xcodeproj`. + +## Adding Tests + +When you add a new unit test file, also add a reference to that file in +shell/platform/darwin/ios/BUILD.gn, under the `sources` list of the +`ios_flutter_test` target. Once it's there, it will execute with the other +tests. diff --git a/testing/ios/IosUnitTests/build_and_run_tests.sh b/testing/ios/IosUnitTests/build_and_run_tests.sh index 076844bd202ea..0c586192d9490 100755 --- a/testing/ios/IosUnitTests/build_and_run_tests.sh +++ b/testing/ios/IosUnitTests/build_and_run_tests.sh @@ -1,7 +1,20 @@ #!/bin/sh + +FLUTTER_ENGINE=ios_debug_sim_unopt + +if [ $# -eq 1 ]; then + FLUTTER_ENGINE=$1 +fi + pushd $PWD cd ../../../.. -./flutter/tools/gn --ios --simulator --unoptimized -ninja -j 100 -C out/ios_debug_sim_unopt + +if [ ! -d "out/$FLUTTER_ENGINE" ]; then + echo "You must GN to generate out/$FLUTTER_ENGINE" + echo "example: ./flutter/tools/gn --ios --simulator --unoptimized" + exit 1 +fi + +autoninja -C out/$FLUTTER_ENGINE ios_test_flutter popd -./run_tests.sh ios_debug_sim_unopt +./run_tests.sh $FLUTTER_ENGINE diff --git a/testing/mock_canvas.cc b/testing/mock_canvas.cc index c1c766b0b610f..300f2d4288c0d 100644 --- a/testing/mock_canvas.cc +++ b/testing/mock_canvas.cc @@ -225,13 +225,6 @@ void MockCanvas::onDrawRRect(const SkRRect&, const SkPaint&) { FML_DCHECK(false); } -void MockCanvas::onDrawBitmap(const SkBitmap&, - SkScalar, - SkScalar, - const SkPaint*) { - FML_DCHECK(false); -} - void MockCanvas::onDrawImage(const SkImage*, SkScalar, SkScalar, @@ -239,14 +232,6 @@ void MockCanvas::onDrawImage(const SkImage*, FML_DCHECK(false); } -void MockCanvas::onDrawBitmapRect(const SkBitmap&, - const SkRect*, - const SkRect&, - const SkPaint*, - SrcRectConstraint) { - FML_DCHECK(false); -} - void MockCanvas::onDrawImageRect(const SkImage*, const SkRect*, const SkRect&, diff --git a/testing/mock_canvas.h b/testing/mock_canvas.h index 17ef16bfa660e..2957783dbe2b5 100644 --- a/testing/mock_canvas.h +++ b/testing/mock_canvas.h @@ -195,11 +195,6 @@ class MockCanvas : public SkCanvasVirtualEnforcer { bool, const SkPaint&) override; void onDrawRRect(const SkRRect&, const SkPaint&) override; - void onDrawBitmapRect(const SkBitmap&, - const SkRect*, - const SkRect&, - const SkPaint*, - SrcRectConstraint) override; void onDrawImage(const SkImage* image, SkScalar x, SkScalar y, @@ -213,21 +208,10 @@ class MockCanvas : public SkCanvasVirtualEnforcer { const SkIRect&, const SkRect&, const SkPaint*) override; - void onDrawBitmap(const SkBitmap& bitmap, - SkScalar x, - SkScalar y, - const SkPaint* paint) override; void onDrawImageLattice(const SkImage*, const Lattice&, const SkRect&, const SkPaint*) override; -#ifdef SK_SUPPORT_LEGACY_DRAWVERTS_VIRTUAL - void onDrawVerticesObject(const SkVertices*, - const SkVertices::Bone[], - int, - SkBlendMode, - const SkPaint&) override {} -#endif void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; diff --git a/testing/resources/performance_overlay_gold_120fps.png b/testing/resources/performance_overlay_gold_120fps.png index 5027397e80482..c19d1eb7208e1 100644 Binary files a/testing/resources/performance_overlay_gold_120fps.png and b/testing/resources/performance_overlay_gold_120fps.png differ diff --git a/testing/resources/performance_overlay_gold_60fps.png b/testing/resources/performance_overlay_gold_60fps.png index 119551f705793..4e18fa15e9436 100644 Binary files a/testing/resources/performance_overlay_gold_60fps.png and b/testing/resources/performance_overlay_gold_60fps.png differ diff --git a/testing/resources/performance_overlay_gold_90fps.png b/testing/resources/performance_overlay_gold_90fps.png index 150123ca046fd..962e2875d4fde 100644 Binary files a/testing/resources/performance_overlay_gold_90fps.png and b/testing/resources/performance_overlay_gold_90fps.png differ diff --git a/testing/run_tests.py b/testing/run_tests.py index 90c3a6067b26f..3fe88f065f4ce 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -108,6 +108,10 @@ def RunCCTests(build_dir, filter): RunEngineExecutable(build_dir, 'client_wrapper_glfw_unittests', filter, shuffle_flags) + RunEngineExecutable(build_dir, 'common_cpp_core_unittests', filter, shuffle_flags) + + RunEngineExecutable(build_dir, 'common_cpp_unittests', filter, shuffle_flags) + RunEngineExecutable(build_dir, 'client_wrapper_unittests', filter, shuffle_flags) # https://github.com/flutter/flutter/issues/36294 @@ -330,6 +334,23 @@ def RunDartTests(build_dir, filter, verbose_dart_snapshot): RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, True) RunDartTest(build_dir, dart_test_file, verbose_dart_snapshot, False) + +def RunFrontEndServerTests(build_dir): + test_dir = os.path.join(buildroot_dir, 'flutter', 'flutter_frontend_server') + dart_tests = glob.glob('%s/test/*_test.dart' % test_dir) + for dart_test_file in dart_tests: + opts = [ + dart_test_file, + os.path.join(build_dir, 'gen', 'frontend_server.dart.snapshot'), + os.path.join(build_dir, 'flutter_patched_sdk')] + RunEngineExecutable( + build_dir, + os.path.join('dart-sdk', 'bin', 'dart'), + None, + flags=opts, + cwd=test_dir) + + def RunConstFinderTests(build_dir): test_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'const_finder', 'test') opts = [ @@ -376,6 +397,7 @@ def main(): dart_filter = args.dart_filter.split(',') if args.dart_filter else None RunDartTests(build_dir, dart_filter, args.verbose_dart_snapshot) RunConstFinderTests(build_dir) + RunFrontEndServerTests(build_dir) if 'java' in types: assert not IsWindows(), "Android engine files can't be compiled on Windows." @@ -389,7 +411,7 @@ def main(): if 'benchmarks' in types and not IsWindows(): RunEngineBenchmarks(build_dir, engine_filter) - if 'engine' in types or 'font-subset' in types: + if ('engine' in types or 'font-subset' in types) and args.variant != 'host_release': RunCmd(['python', 'test.py'], cwd=font_subset_dir) diff --git a/testing/scenario_app/assemble_apk.sh b/testing/scenario_app/assemble_apk.sh index 2671adc80c315..3e75dcc5e9af4 100755 --- a/testing/scenario_app/assemble_apk.sh +++ b/testing/scenario_app/assemble_apk.sh @@ -1,6 +1,15 @@ #!/bin/bash -"${BASH_SOURCE%/*}/compile_android_aot.sh" $1 $2 +set -e + +pushd "${BASH_SOURCE%/*}/../../.." + ./flutter/tools/gn --unopt + ninja -C out/host_debug_unopt sky_engine sky_services +popd + +pushd "${BASH_SOURCE%/*}" + ./compile_android_aot.sh "$1" "$2" +popd pushd "${BASH_SOURCE%/*}/android" ./gradlew assembleDebug --no-daemon diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index c24333a3a8a7f..818d902b3e2e9 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 3DEF491A23C3BE6500184216 /* golden_platform_view_transform_iPhone 8_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DE09E9123C010BD006C9851 /* golden_platform_view_transform_iPhone 8_simulator.png */; }; 59A97FD8236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 59A97FD7236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png */; }; 59A97FDA236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 59A97FD9236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png */; }; + 6402EBD124147BDA00987DCB /* UnobstructedPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */; }; 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */; }; 6816DBA12317573300A51400 /* GoldenImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA02317573300A51400 /* GoldenImage.m */; }; 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */; }; @@ -149,6 +150,7 @@ 3DE09E9223C010BD006C9851 /* golden_platform_view_cliprect_iPhone 8_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone 8_simulator.png"; sourceTree = ""; }; 59A97FD7236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_iPhone SE_simulator.png"; sourceTree = ""; }; 59A97FD9236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png"; sourceTree = ""; }; + 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnobstructedPlatformViewTests.m; sourceTree = ""; }; 6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenPlatformViewTests.h; sourceTree = ""; }; 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoldenPlatformViewTests.m; sourceTree = ""; }; 6816DB9F2317573300A51400 /* GoldenImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenImage.h; sourceTree = ""; }; @@ -245,6 +247,7 @@ 248D76ED22E388380012F0C1 /* ScenariosUITests */ = { isa = PBXGroup; children = ( + 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */, 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */, 3DE09E8B23C010BC006C9851 /* golden_platform_view_clippath_iPhone 8_simulator.png */, 3DE09E9223C010BD006C9851 /* golden_platform_view_cliprect_iPhone 8_simulator.png */, @@ -488,6 +491,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6402EBD124147BDA00987DCB /* UnobstructedPlatformViewTests.m in Sources */, 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */, 6816DBA12317573300A51400 /* GoldenImage.m in Sources */, 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */, diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 348889b19b856..9bd732f647039 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -29,6 +29,13 @@ - (BOOL)application:(UIApplication*)application // the launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. NSDictionary* launchArgsMap = @{ @"--platform-view" : @"platform_view", + @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", + @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", + @"--platform-view-partial-intersection" : @"platform_view_partial_intersection", + @"--platform-view-one-overlay-two-intersecting-overlays" : + @"platform_view_one_overlay_two_intersecting_overlays", + @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", + @"--platform-view-max-overlays" : @"platform_view_max_overlays", @"--platform-view-multiple" : @"platform_view_multiple", @"--platform-view-multiple-background-foreground" : @"platform_view_multiple_background_foreground", diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m index a08519b46b4ec..a3658e337fcca 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m @@ -49,11 +49,13 @@ - (void)checkGolden { XCUIScreenshot* screenshot = [[XCUIScreen mainScreen] screenshot]; XCTAttachment* attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; + attachment.name = @"new_golden"; attachment.lifetime = XCTAttachmentLifetimeKeepAlways; [self addAttachment:attachment]; if (golden.image) { XCTAttachment* goldenAttachment = [XCTAttachment attachmentWithImage:golden.image]; + attachment.name = @"current_golden"; goldenAttachment.lifetime = XCTAttachmentLifetimeKeepAlways; [self addAttachment:goldenAttachment]; } else { diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m index d791210f22707..3d583e1d5e824 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -25,7 +25,7 @@ - (void)testRejectPolicyUtilTouchesEnded { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { @@ -56,7 +56,7 @@ - (void)testRejectPolicyEager { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { @@ -91,7 +91,7 @@ - (void)testAccept { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m new file mode 100644 index 0000000000000..02e7eee35f098 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -0,0 +1,254 @@ +// 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. + +#import + +@interface UnobstructedPlatformViewTests : XCTestCase + +@end + +@implementation UnobstructedPlatformViewTests + +- (void)setUp { + self.continueAfterFailure = NO; +} + +// A is the layer, which z index is higher than the platform view. +// +--------+ +// | PV | +---+ +// +--------+ | A | +// +---+ +- (void)testNoOverlay { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-no-overlay-intersection" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertFalse(overlay.exists); +} + +// A is the layer above the platform view. +// +-----------------+ +// | PV +---+ | +// | | A | | +// | +---+ | +// +-----------------+ +- (void)testOneOverlay { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 150); + XCTAssertEqual(overlay.frame.origin.y, 150); + XCTAssertEqual(overlay.frame.size.width, 50); + XCTAssertEqual(overlay.frame.size.height, 50); +} + +// A is the layer above the platform view. +// +-----------------+ +// | PV +---+ | +// +-----------| A |-+ +// +---+ +- (void)testOneOverlayPartialIntersection { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-partial-intersection" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 200); + XCTAssertEqual(overlay.frame.origin.y, 250); + XCTAssertEqual(overlay.frame.size.width, 50); + // Half the height of the overlay. + XCTAssertEqual(overlay.frame.size.height, 25); +} + +// A and B are the layers above the platform view. +// +--------------------+ +// | PV +------------+ | +// | | B +-----+ | | +// | +---| A |-+ | +// +----------| |---+ +// +-----+ +- (void)testTwoIntersectingOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-two-intersecting-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 150); + XCTAssertEqual(overlay.frame.origin.y, 150); + XCTAssertEqual(overlay.frame.size.width, 75); + XCTAssertEqual(overlay.frame.size.height, 75); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists); +} + +// A, B, and C are the layers above the platform view. +// +-------------------------+ +// | PV +-----------+ | +// | +---+ | B +-----+ | | +// | | C | +---| A |-+ | +// | +---+ +-----+ | +// +-------------------------+ +- (void)testOneOverlayAndTwoIntersectingOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-one-overlay-two-intersecting-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay1 = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay1.exists); + XCTAssertEqual(overlay1.frame.origin.x, 150); + XCTAssertEqual(overlay1.frame.origin.y, 150); + XCTAssertEqual(overlay1.frame.size.width, 75); + XCTAssertEqual(overlay1.frame.size.height, 75); + + XCUIElement* overlay2 = app.otherElements[@"platform_view[0].overlay[1]"]; + XCTAssertTrue(overlay2.exists); + XCTAssertEqual(overlay2.frame.origin.x, 75); + XCTAssertEqual(overlay2.frame.origin.y, 225); + XCTAssertEqual(overlay2.frame.size.width, 50); + XCTAssertEqual(overlay2.frame.size.height, 50); +} + +// A is the layer, which z index is higher than the platform view. +// +--------+ +// | PV | +---+ +// +--------+ | A | +// +--------+ +---+ +// | PV | +// +--------+ +- (void)testMultiplePlatformViewsWithoutOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-multiple-without-overlays" ]; + [app launch]; + + XCUIElement* platform_view1 = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view1.exists); + XCTAssertEqual(platform_view1.frame.origin.x, 25); + XCTAssertEqual(platform_view1.frame.origin.y, 325); + XCTAssertEqual(platform_view1.frame.size.width, 250); + XCTAssertEqual(platform_view1.frame.size.height, 250); + + XCUIElement* platform_view2 = app.textViews[@"platform_view[1]"]; + XCTAssertTrue(platform_view2.exists); + XCTAssertEqual(platform_view2.frame.origin.x, 25); + XCTAssertEqual(platform_view2.frame.origin.y, 25); + XCTAssertEqual(platform_view2.frame.size.width, 250); + XCTAssertEqual(platform_view2.frame.size.height, 250); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[0]"].exists); + XCTAssertFalse(app.otherElements[@"platform_view[1].overlay[0]"].exists); +} + +// A is the layer above both platform view. +// +------------+ +// | PV +----+ | +// +-----| A |-+ +// +-----| |-+ +// | PV +----+ | +// +------------+ +- (void)testMultiplePlatformViewsWithOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-multiple-background-foreground" ]; + [app launch]; + + XCUIElement* platform_view1 = app.textViews[@"platform_view[8]"]; + XCTAssertTrue(platform_view1.exists); + XCTAssertEqual(platform_view1.frame.origin.x, 25); + XCTAssertEqual(platform_view1.frame.origin.y, 325); + XCTAssertEqual(platform_view1.frame.size.width, 250); + XCTAssertEqual(platform_view1.frame.size.height, 250); + + XCUIElement* platform_view2 = app.textViews[@"platform_view[9]"]; + XCTAssertTrue(platform_view2.exists); + XCTAssertEqual(platform_view2.frame.origin.x, 25); + XCTAssertEqual(platform_view2.frame.origin.y, 25); + XCTAssertEqual(platform_view2.frame.size.width, 250); + XCTAssertEqual(platform_view2.frame.size.height, 250); + + XCUIElement* overlay1 = app.otherElements[@"platform_view[8].overlay[0]"]; + XCTAssertTrue(overlay1.exists); + XCTAssertEqual(overlay1.frame.origin.x, 25); + XCTAssertEqual(overlay1.frame.origin.y, 325); + XCTAssertEqual(overlay1.frame.size.width, 225); + XCTAssertEqual(overlay1.frame.size.height, 175); + + XCUIElement* overlay2 = app.otherElements[@"platform_view[9].overlay[0]"]; + XCTAssertTrue(overlay2.exists); + XCTAssertEqual(overlay2.frame.origin.x, 25); + XCTAssertEqual(overlay2.frame.origin.y, 25); + XCTAssertEqual(overlay2.frame.size.width, 225); + XCTAssertEqual(overlay2.frame.size.height, 250); +} + +// More then two overlays are merged into a single layer. +// +---------------------+ +// | +---+ +---+ +---+ | +// | | A | | B | | C | | +// | +---+ +---+ +---+ | +// | +-------+ | +// +-| D |-----------+ +// +-------+ +- (void)testPlatformViewsMaxOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-max-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 75); + XCTAssertEqual(overlay.frame.origin.y, 85); + XCTAssertEqual(overlay.frame.size.width, 150); + XCTAssertEqual(overlay.frame.size.height, 190); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists); +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png index a193faeb04022..9ec19ab474f03 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png index 793082f8f0f7c..0db030ed20983 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png differ diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 494d6585aa7fe..17e56934cded8 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -19,6 +19,12 @@ import 'src/touches_scenario.dart'; Map _scenarios = { 'animated_color_square': AnimatedColorSquareScenario(window), 'platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_no_overlay_intersection': PlatformViewNoOverlayIntersectionScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_partial_intersection': PlatformViewPartialIntersectionScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_two_intersecting_overlays': PlatformViewTwoIntersectingOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_one_overlay_two_intersecting_overlays': PlatformViewOneOverlayTwoIntersectingOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_multiple_without_overlays': MultiPlatformViewWithoutOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_max_overlays': PlatformViewMaxOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), 'platform_view_cliprect': PlatformViewClipRectScenario(window, 'PlatformViewClipRect', id: 1), 'platform_view_cliprrect': PlatformViewClipRRectScenario(window, 'PlatformViewClipRRect', id: 2), 'platform_view_clippath': PlatformViewClipPathScenario(window, 'PlatformViewClipPath', id: 3), @@ -37,6 +43,7 @@ Map _scenarios = { Scenario _currentScenario = _scenarios['animated_color_square']; void main() { + assert(window.locale != null); window ..onPlatformMessage = _handlePlatformMessage ..onBeginFrame = _onBeginFrame diff --git a/testing/scenario_app/lib/src/animated_color_square.dart b/testing/scenario_app/lib/src/animated_color_square.dart index 8a5e51570926c..f1e9865f55309 100644 --- a/testing/scenario_app/lib/src/animated_color_square.dart +++ b/testing/scenario_app/lib/src/animated_color_square.dart @@ -99,7 +99,7 @@ class _NumberSwinger { } else if (_current <= _begin) { _up = true; } - _current = _up ? _current + 1 : _current - 1; + _current = (_up ? _current + 1 : _current - 1) as T; return _current; } } diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 0dde98fd9eebb..fcea41a65f7d5 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -48,6 +48,224 @@ class PlatformViewScenario extends Scenario with _BasePlatformViewScenarioMixin } } +/// A simple platform view with overlay that doesn't intersect with the platform view. +class PlatformViewNoOverlayIntersectionScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewNoOverlayIntersectionScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + finishBuilderByAddingPlatformViewAndPicture( + builder, + 0, + overlayOffset: const Offset(150, 350), + ); + } +} + +/// A simple platform view with an overlay that partially intersects with the platform view. +class PlatformViewPartialIntersectionScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewPartialIntersectionScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + finishBuilderByAddingPlatformViewAndPicture( + builder, + 0, + overlayOffset: const Offset(150, 250), + ); + } +} + +/// A simple platform view with two overlays that intersect with each other and the platform view. +class PlatformViewTwoIntersectingOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewTwoIntersectingOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// A simple platform view with one overlay and two overlays that intersect with each other and the platform view. +class PlatformViewOneOverlayTwoIntersectingOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewOneOverlayTwoIntersectingOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, 200), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// Two platform views without an overlay intersecting either platform view. +class MultiPlatformViewWithoutOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + MultiPlatformViewWithoutOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + builder.pushOffset(0, 600); + _addPlatformViewtoScene(builder, 0, 500, 500); + builder.pop(); + + _addPlatformViewtoScene(builder, 1, 500, 500); + + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawRect( + const Rect.fromLTRB(0, 0, 100, 1000), + Paint()..color = const Color(0xFFFF0000), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(580, 0), picture); + + builder.pop(); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// A simple platform view with too many overlays result in a single native view. +class PlatformViewMaxOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewMaxOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, 200), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, -80), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + /// Builds a scene with 2 platform views. class MultiPlatformViewScenario extends Scenario with _BasePlatformViewScenarioMixin { /// Creates the PlatformView scenario. @@ -424,12 +642,17 @@ mixin _BasePlatformViewScenarioMixin on Scenario { } // Add a platform view and a picture to the scene, then finish the `sceneBuilder`. - void finishBuilderByAddingPlatformViewAndPicture(SceneBuilder sceneBuilder, int viewId) { + void finishBuilderByAddingPlatformViewAndPicture( + SceneBuilder sceneBuilder, + int viewId, { + Offset overlayOffset, + }) { + overlayOffset ??= const Offset(50, 50); _addPlatformViewtoScene(sceneBuilder, viewId, 500, 500); final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); canvas.drawCircle( - const Offset(50, 50), + overlayOffset, 50, Paint()..color = const Color(0xFFABCDEF), ); diff --git a/testing/symbols/verify_exported.dart b/testing/symbols/verify_exported.dart index 2b822d08fea79..2bd2d236d0e49 100644 --- a/testing/symbols/verify_exported.dart +++ b/testing/symbols/verify_exported.dart @@ -110,8 +110,93 @@ int _checkAndroid(String outPath, String nmPath, Iterable builds) { 'JNI_OnLoad': 'T', '_binary_icudtl_dat_size': 'A', '_binary_icudtl_dat_start': 'D', + // TODO(fxb/47943): Remove these once Clang lld does not expose them. + // arm + '__adddf3': 'T', + '__addsf3': 'T', + '__aeabi_cdcmpeq': 'T', + '__aeabi_cdcmple': 'T', + '__aeabi_cdrcmple': 'T', + '__aeabi_d2lz': 'T', + '__aeabi_d2uiz': 'T', + '__aeabi_d2ulz': 'T', + '__aeabi_dadd': 'T', + '__aeabi_dcmpeq': 'T', + '__aeabi_dcmpge': 'T', + '__aeabi_dcmpgt': 'T', + '__aeabi_dcmple': 'T', + '__aeabi_dcmplt': 'T', + '__aeabi_ddiv': 'T', + '__aeabi_dmul': 'T', + '__aeabi_drsub': 'T', + '__aeabi_dsub': 'T', + '__aeabi_f2d': 'T', + '__aeabi_fadd': 'T', + '__aeabi_frsub': 'T', + '__aeabi_fsub': 'T', + '__aeabi_i2d': 'T', + '__aeabi_i2f': 'T', + '__aeabi_l2d': 'T', + '__aeabi_l2f': 'T', + '__aeabi_lasr': 'T', + '__aeabi_ldivmod': 'T', + '__aeabi_llsl': 'T', + '__aeabi_llsr': 'T', + '__aeabi_ui2d': 'T', + '__aeabi_ui2f': 'T', + '__aeabi_uidiv': 'T', + '__aeabi_uidivmod': 'T', + '__aeabi_ul2d': 'T', + '__aeabi_ul2f': 'T', + '__aeabi_uldivmod': 'T', + '__ashldi3': 'T', + '__ashrdi3': 'T', + '__cmpdf2': 'T', + '__divdf3': 'T', + '__divdi3': 'T', + '__eqdf2': 'T', + '__extendsfdf2': 'T', + '__fixdfdi': 'T', + '__fixunsdfdi': 'T', + '__fixunsdfsi': 'T', + '__floatdidf': 'T', + '__floatdisf': 'T', + '__floatsidf': 'T', + '__floatsisf': 'T', + '__floatundidf': 'T', + '__floatundisf': 'T', + '__floatunsidf': 'T', + '__floatunsisf': 'T', + '__gedf2': 'T', + '__gnu_ldivmod_helper': 'T', + '__gnu_uldivmod_helper': 'T', + '__gtdf2': 'T', + '__ledf2': 'T', + '__lshrdi3': 'T', + '__ltdf2': 'T', + '__muldf3': 'T', + '__nedf2': 'T', + '__subdf3': 'T', + '__subsf3': 'T', + '__udivdi3': 'T', + '__udivsi3': 'T', + // arm64 + '__clz_tab': 'R', + '__udivti3': 'T', + // arm64 && x64 + '__emutls_get_address': 'T', + '__emutls_register_common': 'T', + // jit x86 + '__moddi3': 'T', + '__umoddi3': 'T', }; - if (!const MapEquality().equals(entryMap, expectedSymbols)) { + final Map badSymbols = {}; + for (final String key in entryMap.keys) { + if (entryMap[key] != expectedSymbols[key]) { + badSymbols[key] = entryMap[key]; + } + } + if (badSymbols.isNotEmpty) { print('ERROR: $libFlutter exports the wrong symbols'); print(' Expected $expectedSymbols'); print(' Library has $entryMap.'); diff --git a/third_party/txt/src/minikin/HbFontCache.cpp b/third_party/txt/src/minikin/HbFontCache.cpp index 5fc2f59633a18..d60fcc73730e0 100644 --- a/third_party/txt/src/minikin/HbFontCache.cpp +++ b/third_party/txt/src/minikin/HbFontCache.cpp @@ -105,7 +105,7 @@ hb_font_t* getHbFontLocked(const MinikinFont* minikinFont) { font = hb_font_create_sub_font(parent_font); std::vector variations; for (const FontVariation& variation : minikinFont->GetAxes()) { - variations.push_back({variation.axisTag, variation.value}); + variations.push_back({variation.axisTag, variation.value}); } hb_font_set_variations(font, variations.data(), variations.size()); hb_font_destroy(parent_font); diff --git a/third_party/txt/src/txt/font_collection.cc b/third_party/txt/src/txt/font_collection.cc index 3b550f7f313fb..c35c10a95f7e5 100644 --- a/third_party/txt/src/txt/font_collection.cc +++ b/third_party/txt/src/txt/font_collection.cc @@ -151,7 +151,7 @@ FontCollection::GetMinikinFontCollectionForFamilies( // Search for default font family if no user font families were found. if (minikin_families.empty()) { const auto default_font_families = GetDefaultFontFamilies(); - for (auto family : default_font_families) { + for (const auto& family : default_font_families) { std::shared_ptr minikin_family = FindFontFamilyInManagers(family); if (minikin_family != nullptr) { @@ -166,7 +166,8 @@ FontCollection::GetMinikinFontCollectionForFamilies( return nullptr; } if (enable_font_fallback_) { - for (std::string fallback_family : fallback_fonts_for_locale_[locale]) { + for (const std::string& fallback_family : + fallback_fonts_for_locale_[locale]) { auto it = fallback_fonts_.find(fallback_family); if (it != fallback_fonts_.end()) { minikin_families.push_back(it->second); diff --git a/third_party/txt/src/txt/font_features.cc b/third_party/txt/src/txt/font_features.cc index 0729a035aa9fb..0b7b6abdc2b83 100644 --- a/third_party/txt/src/txt/font_features.cc +++ b/third_party/txt/src/txt/font_features.cc @@ -30,7 +30,7 @@ std::string FontFeatures::GetFeatureSettings() const { std::ostringstream stream; - for (auto kv : feature_map_) { + for (const auto& kv : feature_map_) { if (stream.tellp()) { stream << ','; } diff --git a/tools/clone_flutter.sh b/tools/clone_flutter.sh index cb9f4916bc965..48debdf44475c 100755 --- a/tools/clone_flutter.sh +++ b/tools/clone_flutter.sh @@ -4,13 +4,12 @@ set -x if [[ "$CIRRUS_CI" = false || -z $CIRRUS_CI ]] then - echo "This script is aimed to be run on CI environments. Do not run locally." - exit 1 + echo "Cloning Flutter repo to local machine." fi if [[ -z $ENGINE_PATH ]] then - echo "Engine path should be set to run the script." + echo "Please set ENGINE_PATH environment variable." exit 1 fi @@ -33,10 +32,25 @@ fi LATEST_COMMIT_TIME_ENGINE=`git log -1 --date=local --format="%cd"` echo "Latest commit time on engine found as $LATEST_COMMIT_TIME_ENGINE" -# Do rest of the task in the root directory -cd ~ -mkdir -p $FRAMEWORK_PATH -cd $FRAMEWORK_PATH +# Check if there is an argument added for repo location. +# If not use the location that should be set by Cirrus/LUCI. +FLUTTER_CLONE_REPO_PATH=$1 + +if [[ -z $FLUTTER_CLONE_REPO_PATH ]] +then + if [[ -z $FRAMEWORK_PATH ]] + then + echo "Framework path should be set to run the script." + exit 1 + fi + # Do rest of the task in the root directory + cd ~ + mkdir -p $FRAMEWORK_PATH + cd $FRAMEWORK_PATH +else + cd $FLUTTER_CLONE_REPO_PATH +fi + # Clone the Flutter Framework. git clone https://github.com/flutter/flutter.git cd flutter diff --git a/tools/fuchsia/fuchsia_libs.gni b/tools/fuchsia/fuchsia_libs.gni index 47336f8e48255..75443f96a6575 100644 --- a/tools/fuchsia/fuchsia_libs.gni +++ b/tools/fuchsia/fuchsia_libs.gni @@ -72,63 +72,22 @@ common_libs = [ vulkan_dist = "$fuchsia_sdk_base/dist" +# Note that the other validation libraries in the Fuchsia SDK seem to have a bug right +# now causing crashes, so it is only recommended that we use +# VkLayer_khronos_validation.so until we have a confirmation that they are fixed. vulkan_validation_libs = [ - { - name = "VkLayer_core_validation.so" - path = rebase_path("$vulkan_dist") - }, { name = "VkLayer_khronos_validation.so" path = rebase_path("$vulkan_dist") }, - { - name = "VkLayer_object_lifetimes.so" - path = rebase_path("$vulkan_dist") - }, - { - name = "VkLayer_stateless_validation.so" - path = rebase_path("$vulkan_dist") - }, - { - name = "VkLayer_thread_safety.so" - path = rebase_path("$vulkan_dist") - }, - { - name = "VkLayer_unique_objects.so" - path = rebase_path("$vulkan_dist") - }, ] vulkan_data_dir = "//fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d" vulkan_icds = [ - { - path = rebase_path("$vulkan_data_dir/VkLayer_core_validation.json") - dest = "vulkan/explicit_layer.d/VkLayer_core_validation.json" - }, - { - path = rebase_path("$vulkan_data_dir/VkLayer_object_lifetimes.json") - dest = "vulkan/explicit_layer.d/VkLayer_object_lifetimes.json" - }, - { - path = rebase_path("$vulkan_data_dir/VkLayer_thread_safety.json") - dest = "vulkan/explicit_layer.d/VkLayer_thread_safety.json" - }, - { - path = rebase_path("$vulkan_data_dir/VkLayer_stateless_validation.json") - dest = "vulkan/explicit_layer.d/VkLayer_stateless_validation.json" - }, - { - path = rebase_path("$vulkan_data_dir/VkLayer_unique_objects.json") - dest = "vulkan/explicit_layer.d/VkLayer_unique_objects.json" - }, { path = rebase_path("$vulkan_data_dir/VkLayer_khronos_validation.json") dest = "vulkan/explicit_layer.d/VkLayer_khronos_validation.json" }, - { - path = rebase_path("$vulkan_data_dir/VkLayer_standard_validation.json") - dest = "vulkan/explicit_layer.d/VkLayer_standard_validation.json" - }, ] diff --git a/tools/fuchsia/merge_and_upload_debug_symbols.py b/tools/fuchsia/merge_and_upload_debug_symbols.py index 3bf5d852ca972..b40d91d62a7a5 100755 --- a/tools/fuchsia/merge_and_upload_debug_symbols.py +++ b/tools/fuchsia/merge_and_upload_debug_symbols.py @@ -73,16 +73,25 @@ def ProcessCIPDPackage(upload, cipd_yaml, engine_version, out_dir, target_arch): if tries == num_tries - 1: raise -def CreateTarFile(folder_path, base_dir): - archive_name = os.path.basename(folder_path) - tar_file_path = os.path.join(base_dir, archive_name + '.tar.bz2') - with tarfile.open(tar_file_path, "w:bz2") as archive: - for root, dirs, _ in os.walk(folder_path): - for dir_name in dirs: - dir_path = os.path.join(root, dir_name) - archive.add(dir_path, arcname=dir_name) - return tar_file_path - +# Recursively hardlinks contents from one directory to another, +# skipping over collisions. +def HardlinkContents(dirA, dirB): + for src_dir, _, filenames in os.walk(dirA): + for filename in filenames: + src = os.path.join(src_dir, filename) + dest_dir = os.path.join(dirB, os.path.relpath(src_dir, dirA)) + try: + os.makedirs(dest_dir) + except: + pass + dest = os.path.join(dest_dir, filename) + if os.path.exists(dest): + # The last two path components provide a content address for a .build-id entry. + tokens = os.path.split(dest) + name = os.path.join(tokens[-2], tokens[-1]) + print('%s already exists in destination; skipping linking' % name) + continue + os.link(src, dest) def main(): parser = argparse.ArgumentParser() @@ -114,18 +123,17 @@ def main(): for symbol_dir in symbol_dirs: assert os.path.exists(symbol_dir) and os.path.isdir(symbol_dir) - arch = args.target_arch - out_dir = os.path.join(args.out_dir, - 'flutter-fuchsia-debug-symbols-%s' % arch) + out_dir = args.out_dir + if os.path.exists(out_dir): print 'Directory: %s is not empty, deleting it.' % out_dir shutil.rmtree(out_dir) os.makedirs(out_dir) for symbol_dir in symbol_dirs: - archive_path = CreateTarFile(symbol_dir, out_dir) - print('Created archive: ' + archive_path) + HardlinkContents(symbol_dir, out_dir) + arch = args.target_arch cipd_def = WriteCIPDDefinition(arch, out_dir) ProcessCIPDPackage(args.upload, cipd_def, args.engine_version, out_dir, arch) return 0 diff --git a/tools/gn b/tools/gn index b7cb71763da95..6d47127ca30fe 100755 --- a/tools/gn +++ b/tools/gn @@ -44,9 +44,6 @@ def get_out_dir(args): if args.enable_vulkan: target_dir.append('vulkan') - if args.enable_metal and args.target_os == 'ios': - target_dir.append('metal') - return os.path.join(args.out_dir, 'out', '_'.join(target_dir)) def to_command_line(gn_args): @@ -219,10 +216,14 @@ def to_gn_args(args): gn_args['use_goma'] = False gn_args['goma_dir'] = None - if args.enable_metal: + # Enable Metal on non-simulator iOS builds. + if args.target_os == 'ios' and not args.simulator: gn_args['skia_use_metal'] = True gn_args['shell_enable_metal'] = True - gn_args['allow_deprecated_api_calls'] = True + # Bitcode enabled builds using the current version of the toolchain leak + # C++ symbols decorated with the availability attribute. Disable these + # attributes in release modes till the toolchain is updated. + gn_args['skia_enable_api_available_macro'] = args.runtime_mode != "release" if args.enable_vulkan: # Enable vulkan in the Flutter shell. @@ -320,7 +321,6 @@ def parse_args(args): parser.add_argument('--operator-new-alignment', dest='operator_new_alignment', type=str, default=None) parser.add_argument('--enable-vulkan', action='store_true', default=False) - parser.add_argument('--enable-metal', action='store_true', default=False) parser.add_argument('--enable-fontconfig', action='store_true', default=False) parser.add_argument('--enable-skshaper', action='store_true', default=False) diff --git a/vulkan/BUILD.gn b/vulkan/BUILD.gn index 13781b5a66141..c701763ea121f 100644 --- a/vulkan/BUILD.gn +++ b/vulkan/BUILD.gn @@ -5,17 +5,13 @@ import("//build/fuchsia/sdk.gni") config("vulkan_config") { - if (using_fuchsia_sdk) { - include_dirs = [ "$fuchsia_sdk_root/vulkan/include" ] - } else if (is_fuchsia) { - include_dirs = - [ "//third_party/vulkan_loader_and_validation_layers/include" ] - } else { - include_dirs = [ "//third_party/vulkan/src" ] - } - + include_dirs = [] + defines = [] if (is_fuchsia) { - defines = [ "VK_USE_PLATFORM_FUCHSIA=1" ] + include_dirs += [ "$fuchsia_sdk_root/vulkan/include" ] + defines += [ "VK_USE_PLATFORM_FUCHSIA=1" ] + } else { + include_dirs += [ "//third_party/vulkan/src" ] } } diff --git a/vulkan/vulkan_utilities.cc b/vulkan/vulkan_utilities.cc index d02602104e328..61de451afa283 100644 --- a/vulkan/vulkan_utilities.cc +++ b/vulkan/vulkan_utilities.cc @@ -43,13 +43,10 @@ static std::vector InstanceOrDeviceLayersToEnable( // NOTE: The loader is sensitive to the ordering here. Please do not rearrange // this list. #if OS_FUCHSIA - // Fuchsia uses the updated Vulkan loader and validation layers which no - // longer includes the image validation layer. - const std::vector candidates = { - "VK_LAYER_GOOGLE_threading", "VK_LAYER_LUNARG_parameter_validation", - "VK_LAYER_LUNARG_object_tracker", "VK_LAYER_LUNARG_core_validation", - "VK_LAYER_LUNARG_device_limits", "VK_LAYER_LUNARG_swapchain", - "VK_LAYER_GOOGLE_unique_objects"}; + // The other layers in the Fuchsia SDK seem to have a bug right now causing + // crashes, so it is only recommended that we use VK_LAYER_KHRONOS_validation + // until we have a confirmation that they are fixed. + const std::vector candidates = {"VK_LAYER_KHRONOS_validation"}; #else const std::vector candidates = { "VK_LAYER_GOOGLE_threading", "VK_LAYER_LUNARG_parameter_validation",